diff --git a/.github/workflows/qwen-triage.yml b/.github/workflows/qwen-triage.yml index bf0b38090f0..13a4f8890da 100644 --- a/.github/workflows/qwen-triage.yml +++ b/.github/workflows/qwen-triage.yml @@ -973,7 +973,12 @@ jobs: set -euo pipefail apt-get install -y --no-install-recommends tmux util-linux - npm install -g --registry=https://registry.npmjs.org '@qwen-code/qwen-code@latest' + # Run the global install from RUNNER_TEMP: the persistent workspace + # still holds the PREVIOUS run's checked-out tree here, and npm + # reads a cwd .npmrc — whose settings (script-shell, hooks) a + # --registry flag does not override — into a root-privileged + # install. Same fix as the verify lane. + (cd "${RUNNER_TEMP:?}" && npm install -g --registry=https://registry.npmjs.org '@qwen-code/qwen-code@latest') qwen --version tmux -V @@ -982,7 +987,14 @@ jobs: run: |- set -uo pipefail [ -e .git ] || exit 0 - rm -rf .qwen/tmp/review-pr-* 2>/dev/null || true + # Never descend through a PR-writable parent (same guard as the + # end-of-job cleanup and the verify lane's pre-checkout step). + [ -L .qwen ] && rm -f .qwen + if [ -L .qwen/tmp ]; then + rm -f .qwen/tmp + elif [ -d .qwen/tmp ]; then + rm -rf .qwen/tmp/review-pr-* 2>/dev/null || true + fi git worktree prune -v || true - name: 'Checkout PR merge ref' @@ -1062,6 +1074,42 @@ jobs: exit 1 fi + # `npm ci`/`npm run build` ran the PR's lifecycle scripts as `node` + # in the previous step. A detached postinstall child can outlive that + # step and the one-shot sweep below — re-planting a `*-tmux-*` + # artifact dir after the sweep runs, or tampering with the agent's + # inputs while it executes. Kill anything still running as the build + # user BEFORE any cleanup, same as the verify lane, so the sweep is + # not racing a live process. This also removes the localhost blind + # scan surface the bearer-gated proxy defends against. The proxy + # started further below runs as root, so this cannot touch it. + pkill -KILL -u node 2>/dev/null || true + for _ in 1 2 3; do + pgrep -u node >/dev/null 2>&1 || break + sleep 1 + pkill -KILL -u node 2>/dev/null || true + done + if pgrep -u node >/dev/null 2>&1; then + echo "::error::Processes owned by the build user survived; refusing to start the agent." + exit 1 + fi + + # `npm ci`/`npm run build` just ran the PR's lifecycle scripts, which + # can plant a `tmp/-tmux-/` directory holding a report.md + # and a transcript. The collector below globs `*-tmux-*` and the + # publisher takes the first match, so a planted dir can supply the + # comment's contents. Sweep the lifecycle-script plant channel; the + # runtime channel (PR code the agent launches concurrently) is + # bounded by the symlink strip after collection. find's default + # -P does not follow a symlinked start path, so there is no live + # escape to close; the [ -L tmp ] guard is defence-in-depth against + # a future edit adding a trailing slash or -L, which would follow it. + if [ -L tmp ]; then + echo '::warning::tmp is a symlink; skipping pre-agent tmux sweep' + else + find tmp -maxdepth 2 -type d -name '*-tmux-*' -exec rm -rf {} + 2>/dev/null || true + fi + # Bypass the runner proxy before launching qwen: the proxy cuts the # SSE stream to the model host, and qwen reads HTTP(S)_PROXY directly # without honoring NO_PROXY. Clear proxy env for qwen itself while @@ -1133,17 +1181,37 @@ jobs: unset ACTIONS_RUNTIME_TOKEN ACTIONS_RUNTIME_URL ACTIONS_CACHE_URL start_openai_proxy() { - local proxy_port proxy_script - proxy_port=8787 - proxy_script="${RUNNER_TEMP:-/tmp}/qwen-openai-proxy.js" + local proxy_port proxy_script proxy_nonce port_file + # PR lifecycle code ran before this step and could have left a + # server on a fixed port: the real proxy would die with + # EADDRINUSE while the health probe succeeded against the + # squatter, and qwen would take its completions. Proven + # exploitable on the verify lane; same defences here — ephemeral + # port reported through a root-owned file, a per-run nonce the + # health endpoint must echo, a bearer token only the agent + # knows, and a liveness check on our PID. + proxy_script="${RUNNER_TEMP:-/tmp}/qwen-openai-proxy-tmux.js" + port_file="${RUNNER_TEMP:-/tmp}/qwen-openai-proxy-tmux.port" + proxy_nonce="$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')" + # NOT in the `local` list above on purpose: the agent env below + # reads it as OPENAI_API_KEY and the proxy receives it as + # PROXY_TOKEN. Uppercase + export mirror the verify lane, so a + # tidy that adds it to `local` cannot silently blank the agent's + # key and 401 every completion. + PROXY_TOKEN="$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')" + export PROXY_TOKEN + rm -f "$port_file" cat > "$proxy_script" <<'NODE' const http = require('node:http'); + const { writeFileSync } = require('node:fs'); const { Readable } = require('node:stream'); - const port = Number(process.argv[2]); + const portFile = process.argv[2]; const baseUrl = process.env.REVIEW_OPENAI_BASE_URL; const apiKey = process.env.REVIEW_OPENAI_API_KEY; - if (!baseUrl || !apiKey || !Number.isInteger(port)) { + const nonce = process.env.QWEN_PROXY_NONCE; + const token = process.env.PROXY_TOKEN; + if (!baseUrl || !apiKey || !portFile || !nonce || !token) { console.error('missing proxy configuration'); process.exit(1); } @@ -1153,11 +1221,13 @@ jobs: const server = http.createServer(async (req, res) => { if (req.url === '/__health') { - res.writeHead(204); - res.end(); + // Identity, not just liveness: a squatter cannot know the nonce. + res.writeHead(200, { 'content-type': 'text/plain' }); + res.end(nonce); return; } + let timer; try { const incoming = new URL(req.url || '/', 'http://127.0.0.1'); const target = new URL(base.origin); @@ -1179,6 +1249,14 @@ jobs: return; } + // Only the agent knows this run's token; anything else on the + // runner that finds the port cannot get the key used. + if (req.headers.authorization !== `Bearer ${token}`) { + res.writeHead(401, { 'content-type': 'text/plain' }); + res.end('proxy: unauthorized\n'); + return; + } + const headers = new Headers(req.headers); headers.delete('host'); headers.delete('content-length'); @@ -1194,7 +1272,7 @@ jobs: } const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), 120_000); + timer = setTimeout(() => controller.abort(), 120_000); let upstream; try { upstream = await fetch(target, { ...init, signal: controller.signal }); @@ -1205,9 +1283,12 @@ jobs: return; } throw error; - } finally { - clearTimeout(timer); } + // NOTE: the timer is deliberately NOT cleared here. fetch() + // resolves on HEADERS, and the armed timer doubles as the + // first idle window for the body; the per-chunk handler below + // refreshes it. Clearing it here would leave no guard on an + // upstream that sends headers and then stalls. const responseHeaders = {}; upstream.headers.forEach((value, key) => { const lower = key.toLowerCase(); @@ -1217,37 +1298,82 @@ jobs: }); res.writeHead(upstream.status, responseHeaders); if (upstream.body) { - Readable.fromWeb(upstream.body).pipe(res); + const body = Readable.fromWeb(upstream.body); + const done = () => clearTimeout(timer); + body.on('end', done); + // When the idle watchdog fires mid-body it aborts the + // upstream, which surfaces here as an error: tear down the + // downstream response too, or the client sits on a silent + // socket until its own idle timer. + body.on('error', () => { + done(); + res.destroy(); + }); + // A downstream disconnect must abort the upstream request + // too, or the stalled fetch keeps the socket alive. + res.on('close', () => { + done(); + controller.abort(); + }); + body.pipe(res); + // Idle, not total: qwen tolerates minutes of silence inside + // one stream (DEFAULT_STREAM_IDLE_TIMEOUT_MS), so a total + // cap would cut healthy long completions. Refresh per chunk; + // attached after pipe() because a 'data' listener flips the + // stream to flowing mode. + body.on('data', () => { + clearTimeout(timer); + timer = setTimeout(() => controller.abort(), 120_000); + }); } else { + clearTimeout(timer); res.end(); } } catch (error) { + clearTimeout(timer); + // The message can carry resolved hosts, IPs and TLS detail. + // The agent needs to know the call failed, not the topology. + console.error('proxy upstream failure:', error); res.writeHead(502, { 'content-type': 'text/plain' }); - res.end(`proxy error: ${error instanceof Error ? error.message : String(error)}\n`); + res.end('proxy error: upstream request failed\n'); } }); - server.listen(port, '127.0.0.1'); + // Port 0: let the OS choose, then publish it where only root can write. + server.listen(0, '127.0.0.1', () => { + writeFileSync(portFile, String(server.address().port)); + }); NODE REVIEW_OPENAI_API_KEY="$REVIEW_OPENAI_API_KEY" \ REVIEW_OPENAI_BASE_URL="$REVIEW_OPENAI_BASE_URL" \ - node "$proxy_script" "$proxy_port" & + QWEN_PROXY_NONCE="$proxy_nonce" \ + node "$proxy_script" "$port_file" & OPENAI_PROXY_PID=$! trap 'kill "$OPENAI_PROXY_PID" 2>/dev/null || true' EXIT - for _ in 1 2 3 4 5; do - if curl -fsS "http://127.0.0.1:${proxy_port}/__health" >/dev/null; then - break - fi + proxy_port='' + for _ in 1 2 3 4 5 6 7 8 9 10; do if ! kill -0 "$OPENAI_PROXY_PID" 2>/dev/null; then echo "::error::OpenAI proxy exited before becoming ready" exit 1 fi + if [ -z "$proxy_port" ] && [ -s "$port_file" ]; then + proxy_port="$(tr -cd '0-9' < "$port_file")" + fi + if [ -n "$proxy_port" ] && + [ "$(curl -fsS "http://127.0.0.1:${proxy_port}/__health" 2>/dev/null)" = "$proxy_nonce" ]; then + break + fi + proxy_port='' sleep 1 done - if ! curl -fsS "http://127.0.0.1:${proxy_port}/__health" >/dev/null; then - echo "::error::OpenAI proxy did not become ready" + # All three must hold: the PID we started is alive, the port it + # reported, and the nonce echoed back. + if [ -z "$proxy_port" ] || + ! kill -0 "$OPENAI_PROXY_PID" 2>/dev/null || + [ "$(curl -fsS "http://127.0.0.1:${proxy_port}/__health" 2>/dev/null)" != "$proxy_nonce" ]; then + echo "::error::OpenAI proxy did not become ready (or another process answered on its port)" exit 1 fi @@ -1283,7 +1409,7 @@ jobs: "GITHUB_REPOSITORY=$GITHUB_REPOSITORY" "GITHUB_TOKEN=" "GH_TOKEN=" - "OPENAI_API_KEY=qwen-loopback-proxy" + "OPENAI_API_KEY=$PROXY_TOKEN" "OPENAI_BASE_URL=$LOCAL_OPENAI_BASE_URL" "NO_PROXY=${NO_PROXY:-}" "no_proxy=${no_proxy:-}" @@ -1309,6 +1435,10 @@ jobs: # Collect the skill's narrative artifacts (report.md, readable logs) # from the workspace tmp/ into the upload dir. find tmp -maxdepth 2 -type d -name '*-tmux-*' -exec cp -r {} "$RUNNER_TEMP/tmux-results/" \; 2>/dev/null || true + # cp -r copies symlinks as symlinks (no deref), but + # actions/upload-artifact FOLLOWS them — a node-planted link would + # exfiltrate whatever it points at into the artifact. Drop links. + find "$RUNNER_TEMP/tmux-results" -type l -delete 2>/dev/null || true if [ "$EXIT_CODE" -eq 124 ]; then VERDICT='timeout' @@ -1347,8 +1477,21 @@ jobs: run: |- set -uo pipefail [ -e .git ] || exit 0 - rm -rf .qwen/tmp/review-pr-* 2>/dev/null || true - find tmp -maxdepth 2 -type d -name '*-tmux-*' -exec rm -rf {} + 2>/dev/null || true + # Never descend through a PR-writable parent: PR code ran in this + # workspace, so `.qwen` or `.qwen/tmp` can be a symlink pointing + # outside it (verified on the verify lane: the glob then deletes + # the link target's contents as root). + [ -L .qwen ] && rm -f .qwen + if [ -L .qwen/tmp ]; then + rm -f .qwen/tmp + elif [ -d .qwen/tmp ]; then + rm -rf .qwen/tmp/review-pr-* 2>/dev/null || true + fi + if [ -L tmp ]; then + echo '::warning::tmp is a symlink; skipping end-of-job tmux sweep' + else + find tmp -maxdepth 2 -type d -name '*-tmux-*' -exec rm -rf {} + 2>/dev/null || true + fi git worktree prune -v || true # Post the tmux verdict back to the PR. Runs on a clean GitHub-hosted runner @@ -1368,6 +1511,20 @@ jobs: (needs.tmux-testing.result == 'success' && needs.tmux-testing.outputs.verdict != '' && needs.tmux-testing.outputs.verdict != 'n/a')) + # Per-RUN group, deliberately not per-PR: a GitHub concurrency group + # holds at most one running plus one pending job, and a newer pending job + # REPLACES the older one even with cancel-in-progress: false — so a + # per-PR group would let a second run cancel a completed run's pending + # publisher and drop its report. Overlap between publishers is instead + # made safe by the bot-owned prefix-match dedup below. Same shape as + # publish-verify. + concurrency: + group: "${{ format('{0}-publish-tmux-{1}', github.workflow, github.run_id) }}" + cancel-in-progress: false + # Downloads one artifact and posts one comment; without this it would + # inherit the 360-minute default and a hung gh call could hold a hosted + # runner for six hours. Same bound as publish-verify. + timeout-minutes: 10 runs-on: 'ubuntu-latest' permissions: pull-requests: 'write' @@ -1415,16 +1572,32 @@ jobs: local summary="$1" file="$2" max="$3" content truncated='' summary_html [ -n "$file" ] && [ -f "$file" ] || return 0 summary_html="$(printf '%s' "$summary" | html_escape)" - if [ "$(wc -c < "$file")" -gt "$max" ]; then - truncated=$'\n\n...truncated -- full log in the run artifacts.' - fi - if ! content="$( + # Escape FIRST, then cap. Escaping inflates every & < > by 4-5 + # bytes, so a raw-side cap can push the assembled body past + # GitHub's 65,536-char comment limit, 422 the post, and leave no + # comment at all. Same fix the verify lane carries; truncation is + # done on a character boundary via node, because BSD `iconv -c` + # passes an incomplete trailing UTF-8 sequence through unchanged. + local esc_file="${TMPDIR:-/tmp}/tmux-emit-$$" + if ! ( set -o pipefail - head -c "$max" "$file" | tr -d '\000' | html_escape - )"; then + head -c 400000 "$file" | tr -d '\000' | html_escape > "$esc_file" + ); then echo "::warning::emit_block failed while rendering $summary; see run artifacts." >&2 + rm -f "$esc_file" content='Log could not be rendered; see run artifacts.' - elif [ -n "$truncated" ]; then + else + if [ "$(wc -c < "$esc_file")" -gt "$max" ] || + [ "$(wc -c < "$file")" -gt 400000 ]; then + truncated=$'\n\n...truncated -- full log in the run artifacts.' + fi + content="$(node -e ' + const fs = require("node:fs"); + const [file, max] = process.argv.slice(1); + const buf = fs.readFileSync(file).subarray(0, Number(max)); + process.stdout.write(new TextDecoder("utf-8").decode(buf).replace(/�+$/, "")); + ' "$esc_file" "$max")" || content="$(head -c "$max" "$esc_file")" + rm -f "$esc_file" content="${content}${truncated}" fi printf '
\n%s\n\n
\n' "$summary_html"
@@ -1476,8 +1649,8 @@ jobs:
               printf '%s\n' '— _Qwen Code · tmux real-user testing_'
             } > "$BODY_FILE"
           else
-            REPORT="$(find tmux-results -name 'report.md' 2>/dev/null | head -1 || true)"
-            TRANSCRIPT="$(find tmux-results -name 'tmux-readable-full.log' 2>/dev/null | head -1 || true)"
+            REPORT="$(find tmux-results -mindepth 2 -type f -path '*-tmux-*/report.md' 2>/dev/null | sort | head -1 || true)"
+            TRANSCRIPT="$(find tmux-results -mindepth 2 -type f -path '*-tmux-*/tmux-readable-full.log' 2>/dev/null | sort | head -1 || true)"
             if [ -z "$REPORT" ] && [ -z "$TRANSCRIPT" ]; then
               MISSING_ARTIFACTS_NOTE='No report.md or tmux-readable-full.log was found in tmux-results, so detailed report sections are omitted.'
               echo "::warning::${MISSING_ARTIFACTS_NOTE}"
@@ -1522,13 +1695,24 @@ jobs:
           fi
 
           # Dedup: update an existing tmux comment if one is already present.
-          if ! EXISTING="$(
+          # Only BOT-OWNED comments STARTING with the marker are candidates,
+          # so a marker a human reviewer quotes cannot divert the bot into
+          # PATCHing (and overwriting) their comment with the write PAT.
+          # Fail CLOSED on identity failure: an empty login would widen the
+          # filter to every user's comments. Same discipline as publish-verify.
+          EXISTING=''
+          if ! BOT_LOGIN="$(gh api user --jq '.login')" || [ -z "$BOT_LOGIN" ]; then
+            echo "::warning::Could not resolve the bot identity; posting a fresh tmux comment instead of reusing one."
+            BOT_LOGIN=''
+          fi
+          if [ -n "$BOT_LOGIN" ] && ! EXISTING="$(
             # -F would otherwise make gh api default to POST.
             gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
               --method GET \
               --paginate \
               -F per_page=100 \
-              | jq -sr '[.[][] | select(.body | contains(""))] | last | .id // empty'
+              | jq -sr --arg bot "$BOT_LOGIN" \
+                '[.[][] | select((.body | startswith("")) and .user.login == $bot)] | last | .id // empty'
           )"; then
             echo "::warning::Failed to look up existing tmux comments; will create a new one."
             EXISTING=""
@@ -2249,6 +2433,7 @@ jobs:
               return;
             }
 
+            let timer;
             try {
               const incoming = new URL(req.url || '/', 'http://127.0.0.1');
               const target = new URL(base.origin);
@@ -2293,7 +2478,7 @@ jobs:
               }
 
               const controller = new AbortController();
-              const timer = setTimeout(() => controller.abort(), 120_000);
+              timer = setTimeout(() => controller.abort(), 120_000);
               let upstream;
               try {
                 upstream = await fetch(target, { ...init, signal: controller.signal });
@@ -2306,9 +2491,10 @@ jobs:
                 throw error;
               }
               // NOTE: the timer is deliberately NOT cleared here. fetch()
-              // resolves on HEADERS, so clearing now would let an upstream
-              // that stalls mid-body hang until the outer 25-minute
-              // watchdog. It is cleared when the body ends or errors below.
+              // resolves on HEADERS, and the armed timer doubles as the
+              // first idle window for the body; the per-chunk handler below
+              // refreshes it. Clearing it here would leave no guard on an
+              // upstream that sends headers and then stalls.
               const responseHeaders = {};
               upstream.headers.forEach((value, key) => {
                 const lower = key.toLowerCase();
@@ -2321,7 +2507,14 @@ jobs:
                 const body = Readable.fromWeb(upstream.body);
                 const done = () => clearTimeout(timer);
                 body.on('end', done);
-                body.on('error', done);
+                // When the idle watchdog fires mid-body it aborts the
+                // upstream, which surfaces here as an error: tear down the
+                // downstream response too, or the client sits on a silent
+                // socket until its own idle timer.
+                body.on('error', () => {
+                  done();
+                  res.destroy();
+                });
                 // A downstream disconnect must abort the upstream request
                 // too, or the stalled fetch keeps the socket alive.
                 res.on('close', () => {
@@ -2329,11 +2522,21 @@ jobs:
                   controller.abort();
                 });
                 body.pipe(res);
+                // Idle, not total: qwen tolerates minutes of silence inside
+                // one stream (DEFAULT_STREAM_IDLE_TIMEOUT_MS), so a total
+                // cap would cut healthy long completions. Refresh per chunk;
+                // attached after pipe() because a 'data' listener flips the
+                // stream to flowing mode.
+                body.on('data', () => {
+                  clearTimeout(timer);
+                  timer = setTimeout(() => controller.abort(), 120_000);
+                });
               } else {
                 clearTimeout(timer);
                 res.end();
               }
             } catch (error) {
+              clearTimeout(timer);
               // The message can carry resolved hosts, IPs and TLS detail.
               // The agent needs to know the call failed, not the topology.
               console.error('proxy upstream failure:', error);
diff --git a/scripts/tests/qwen-triage-workflow.test.js b/scripts/tests/qwen-triage-workflow.test.js
index 84e8b00f412..2e3b41b82a1 100644
--- a/scripts/tests/qwen-triage-workflow.test.js
+++ b/scripts/tests/qwen-triage-workflow.test.js
@@ -62,6 +62,75 @@ function job(name) {
     : workflow.slice(start, start + 1 + nextJob);
 }
 
+// Spawns the real proxy against a streaming upstream (20 chunks, 200 ms
+// apart = 4 s total) and a stalling upstream (headers + one chunk, then
+// silence), with the proxy's 120 s watchdog shortened to 1.5 s. The healthy
+// stream spans longer than the idle window while each gap stays under it, so
+// it arrives in full only if the watchdog is idle (refreshed per chunk) and
+// not a total cap; and a mid-body stall must CLOSE the downstream response,
+// not strand the client on a silent socket until its own timeout.
+function runProxyWatchdogTest(proxy) {
+  const dir = mkdtempSync(join(tmpdir(), 'proxy-watchdog-'));
+  try {
+    writeFileSync(
+      join(dir, 'proxy.js'),
+      proxy.replace(/^ {10}/gm, '').replaceAll('120_000', '1500'),
+    );
+    writeFileSync(
+      join(dir, 'stream.js'),
+      [
+        "const http = require('node:http');",
+        "const fs = require('node:fs');",
+        'const NL = String.fromCharCode(10);',
+        'const ticks = Number(process.argv[3]);',
+        'const tickMs = Number(process.argv[4]);',
+        'const s = http.createServer((q, r) => {',
+        "  r.writeHead(200, { 'content-type': 'text/event-stream' });",
+        '  let i = 0;',
+        '  const iv = setInterval(() => {',
+        "    r.write('data: ' + i++ + NL + NL);",
+        '    if (i >= ticks) { clearInterval(iv); r.end(); }',
+        '  }, tickMs);',
+        '});',
+        "s.listen(0, '127.0.0.1', () => fs.writeFileSync(process.argv[2], String(s.address().port)));",
+      ].join('\n'),
+    );
+    writeFileSync(
+      join(dir, 'stall.js'),
+      [
+        "const http = require('node:http');",
+        "const fs = require('node:fs');",
+        'const NL = String.fromCharCode(10);',
+        'const s = http.createServer((q, r) => {',
+        "  r.writeHead(200, { 'content-type': 'text/event-stream' });",
+        "  r.write('data: 0' + NL + NL);",
+        '});',
+        "s.listen(0, '127.0.0.1', () => fs.writeFileSync(process.argv[2], String(s.address().port)));",
+      ].join('\n'),
+    );
+    const driver = [
+      'set -u',
+      'node "$1/stream.js" "$1/stream.port" 20 200 & STREAM=$!',
+      'node "$1/stall.js" "$1/stall.port" & STALL=$!',
+      'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/stream.port" ] && [ -s "$1/stall.port" ] && break; sleep 0.3; done',
+      'REVIEW_OPENAI_BASE_URL="http://127.0.0.1:$(cat "$1/stream.port")/v1" REVIEW_OPENAI_API_KEY=k QWEN_PROXY_NONCE=n0nce PROXY_TOKEN=t0ken node "$1/proxy.js" "$1/px.port" & PX=$!',
+      'REVIEW_OPENAI_BASE_URL="http://127.0.0.1:$(cat "$1/stall.port")/v1" REVIEW_OPENAI_API_KEY=k QWEN_PROXY_NONCE=n0nce PROXY_TOKEN=t0ken node "$1/proxy.js" "$1/px2.port" & PX2=$!',
+      'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/px.port" ] && [ -s "$1/px2.port" ] && break; sleep 0.3; done',
+      'P="$(cat "$1/px.port")"; P2="$(cat "$1/px2.port")"',
+      'echo "chunks=$(curl -sS --max-time 15 -X POST -H "Authorization: Bearer t0ken" "http://127.0.0.1:$P/v1/chat/completions" | grep -c "^data:")"',
+      'curl -sS -o /dev/null --max-time 10 -X POST -H "Authorization: Bearer t0ken" "http://127.0.0.1:$P2/v1/chat/completions"',
+      'echo "stall_exit=$?"',
+      'kill $STREAM $STALL $PX $PX2 2>/dev/null',
+    ].join('\n');
+    return spawnSync('bash', ['-c', driver, '_', dir], {
+      encoding: 'utf8',
+      timeout: 60000,
+    }).stdout;
+  } finally {
+    rmSync(dir, { recursive: true, force: true });
+  }
+}
+
 describe('qwen-triage tmux workflow', () => {
   it('does not require fork PR authors to have write permission for automatic triage', () => {
     const precheckJob = job('precheck-pr');
@@ -110,7 +179,10 @@ describe('qwen-triage tmux workflow', () => {
     expect(postStep).toContain('html_escape()');
     expect(postStep).toContain("tr -d '\\000'");
     expect(postStep).toContain('Log could not be rendered');
-    expect(postStep).toContain('if ! content="$(');
+    // The escape now writes to a file and the cap is applied afterwards, so
+    // the guarantee is "a render failure is caught", not the old inline
+    // capture shape. See the tmux-lane-parity suite for the cap itself.
+    expect(postStep).toContain('html_escape > "$esc_file"');
     expect(postStep).toContain('set -o pipefail');
     expect(postStep).toContain('::warning::emit_block failed');
     expect(postStep).toContain(
@@ -2003,6 +2075,18 @@ describe('qwen-triage verify maintainer-review round', () => {
           "s.listen(0, '127.0.0.1', () => fs.writeFileSync(process.argv[2], String(s.address().port)));",
         ].join('\n'),
       );
+      writeFileSync(
+        join(dir, 'deadport.js'),
+        [
+          "const net = require('node:net');",
+          "const fs = require('node:fs');",
+          'const s = net.createServer();',
+          "s.listen(0, '127.0.0.1', () => {",
+          '  const p = s.address().port;',
+          '  s.close(() => fs.writeFileSync(process.argv[2], String(p)));',
+          '});',
+        ].join('\n'),
+      );
       const driver = [
         'set -u',
         'node "$1/upstream.js" "$1/up.port" & UP=$!',
@@ -2018,7 +2102,16 @@ describe('qwen-triage verify maintainer-review round', () => {
         'echo "wrong=$(curl -s -o /dev/null -w %{http_code} -X POST -H "authorization: Bearer nope" -d {} "$U")"',
         'echo "right=$(curl -s -o /dev/null -w %{http_code} -X POST -H "authorization: Bearer tok456" -d {} "$U")"',
         'echo "otherpath=$(curl -s -o /dev/null -w %{http_code} -X POST -H "authorization: Bearer tok456" -d {} "http://127.0.0.1:$P/v1/models")"',
-        'kill $UP $PX 2>/dev/null',
+        'node "$1/deadport.js" "$1/dead.port"',
+        'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/dead.port" ] && break; sleep 0.3; done',
+        'DEAD="$(cat "$1/dead.port")"',
+        'REVIEW_OPENAI_BASE_URL="http://127.0.0.1:$DEAD/v1" REVIEW_OPENAI_API_KEY=realkey \\',
+        '  QWEN_PROXY_NONCE=nonce123 PROXY_TOKEN=tok456 node "$1/proxy.js" "$1/px2.port" & PX2=$!',
+        'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/px2.port" ] && break; sleep 0.3; done',
+        'P2="$(cat "$1/px2.port")"',
+        'echo "dead=$(curl -s -o /dev/null -w %{http_code} -X POST -H "authorization: Bearer tok456" -d {} "http://127.0.0.1:$P2/v1/chat/completions")"',
+        'echo "dead2=$(curl -s -o /dev/null -w %{http_code} -X POST -H "authorization: Bearer tok456" -d {} "http://127.0.0.1:$P2/v1/chat/completions")"',
+        'kill $UP $PX $PX2 2>/dev/null',
       ].join('\n');
       const out = spawnSync('bash', ['-c', driver, '_', dir], {
         encoding: 'utf8',
@@ -2032,11 +2125,36 @@ describe('qwen-triage verify maintainer-review round', () => {
       // ...and reachable with it, on the one allowed route.
       expect(out).toContain('right=200');
       expect(out).toContain('otherpath=403');
+      // A dead upstream must surface as a 502 the agent can read, not crash
+      // the proxy: the outer catch clears the hoisted timer (a ReferenceError
+      // here would kill the process and turn qwen's next completion into a
+      // false fail verdict), and the process serves the following request.
+      expect(out).toContain('dead=502');
+      expect(out).toContain('dead2=502');
     } finally {
       rmSync(dir, { recursive: true, force: true });
     }
   });
 
+  // The watchdog must be an IDLE timer, not a total one: fetch() resolves on
+  // headers and a completion can stream for minutes (qwen tolerates 240 s of
+  // silence, DEFAULT_STREAM_IDLE_TIMEOUT_MS). A total cap truncated healthy
+  // long completions, and firing it mid-body never terminated the downstream
+  // response, so the client sat on a silent socket.
+  it('treats the verify proxy watchdog as idle and ends a stalled response', () => {
+    const runStep = stepIn('verify', 'Run verification agent');
+    const proxy = runStep.match(/<<'NODE'\n([\s\S]*?)\n\s*NODE\n/)?.[1];
+    expect(proxy).toBeTruthy();
+    const out = runProxyWatchdogTest(proxy);
+    // 20 chunks at 200 ms span 4 s, longer than the 1.5 s idle window, yet
+    // all arrive: the watchdog refreshes per chunk, so a healthy stream is
+    // not cut.
+    expect(out).toContain('chunks=20');
+    // A mid-body stall closes the response (curl 18), not a hang until the
+    // client's own timeout (curl 28).
+    expect(out).toContain('stall_exit=18');
+  });
+
   // GitHub cancels the OLDER pending run in a concurrency group, so the
   // requester's own /verify proceeds — the earlier "queued behind other
   // runs" notice had that backwards and warned the wrong person. The real
@@ -2049,6 +2167,12 @@ describe('qwen-triage verify maintainer-review round', () => {
     expect(publishJob).toContain(
       'needs.verify.outputs.pr_number || github.event.issue.number',
     );
+    // Same one-line class in the tmux sibling: a job cancelled while
+    // pending never evaluates its outputs either, so without the fallback
+    // publish-tmux hits the same null guard and posts nothing.
+    expect(job('publish-tmux')).toContain(
+      'needs.tmux-testing.outputs.pr_number || github.event.issue.number',
+    );
     // ...and the step that warned on the inverted premise is gone.
     expect(job('authorize')).not.toContain('Report saturated verify queue');
 
@@ -2178,8 +2302,338 @@ describe('qwen-triage verify maintainer-review round', () => {
   // Upstream failure text can name resolved hosts and TLS detail; the agent
   // only needs to know the call failed.
   it('does not forward upstream error text to the agent', () => {
-    const runStep = stepIn('verify', 'Run verification agent');
-    expect(runStep).toContain("res.end('proxy error: upstream request failed");
-    expect(runStep).not.toContain('proxy error: ${error instanceof Error');
+    // Both lanes share the proxy design and must both keep upstream
+    // topology out of the agent's error text.
+    for (const [jobName, stepName] of [
+      ['verify', 'Run verification agent'],
+      ['tmux-testing', 'Run tmux real-user testing'],
+    ]) {
+      const runStep = stepIn(jobName, stepName);
+      expect(runStep).toContain(
+        "res.end('proxy error: upstream request failed",
+      );
+      expect(runStep).not.toContain('proxy error: ${error instanceof Error');
+    }
+  });
+});
+
+describe('qwen-triage tmux lane parity', () => {
+  // The verify lane earned these controls the hard way; the tmux lane
+  // executes the same untrusted PR code on the same persistent pool, so
+  // leaving them out was a gap rather than a scope boundary.
+
+  // A fixed proxy port is squattable by a detached lifecycle process: the
+  // real proxy dies EADDRINUSE while the health probe succeeds against the
+  // squatter, and the agent takes ITS chat completions.
+  it('binds the tmux model proxy to an ephemeral port and authenticates it', () => {
+    const runStep = stepIn('tmux-testing', 'Run tmux real-user testing');
+    expect(runStep).not.toContain('proxy_port=8787');
+    expect(runStep).toContain("server.listen(0, '127.0.0.1'");
+    expect(runStep).toContain('QWEN_PROXY_NONCE');
+    expect(runStep).toContain('!= "$proxy_nonce"');
+    expect(runStep).toContain('kill -0 "$OPENAI_PROXY_PID"');
+    expect(runStep).toContain('PROXY_TOKEN');
+    expect(runStep).toContain('proxy: unauthorized');
+    // The agent must actually present this run's token: reverting the env
+    // wire to a literal makes every completion 401 and turns the verdict
+    // into a false 'fail'. Assert the wire, not just the gate's presence.
+    expect(runStep).toContain('"OPENAI_API_KEY=$PROXY_TOKEN"');
+
+    // Execute the real proxy and prove the nonce + bearer token work.
+    const proxy = runStep.match(/<<'NODE'\n([\s\S]*?)\n\s*NODE\n/)?.[1];
+    expect(proxy).toBeTruthy();
+    const dir = mkdtempSync(join(tmpdir(), 'tmux-proxy-'));
+    try {
+      writeFileSync(join(dir, 'proxy.js'), proxy.replace(/^ {10}/gm, ''));
+      writeFileSync(
+        join(dir, 'upstream.js'),
+        [
+          "const http = require('node:http');",
+          "const fs = require('node:fs');",
+          "const s = http.createServer((q, r) => { r.writeHead(200); r.end('{}'); });",
+          "s.listen(0, '127.0.0.1', () => fs.writeFileSync(process.argv[2], String(s.address().port)));",
+        ].join('\n'),
+      );
+      writeFileSync(
+        join(dir, 'deadport.js'),
+        [
+          "const net = require('node:net');",
+          "const fs = require('node:fs');",
+          'const s = net.createServer();',
+          "s.listen(0, '127.0.0.1', () => {",
+          '  const p = s.address().port;',
+          '  s.close(() => fs.writeFileSync(process.argv[2], String(p)));',
+          '});',
+        ].join('\n'),
+      );
+      const driver = [
+        'set -u',
+        'node "$1/upstream.js" "$1/up.port" & UP=$!',
+        'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/up.port" ] && break; sleep 0.3; done',
+        'REVIEW_OPENAI_BASE_URL="http://127.0.0.1:$(cat "$1/up.port")/v1" \\',
+        '  REVIEW_OPENAI_API_KEY=k QWEN_PROXY_NONCE=n0nce PROXY_TOKEN=t0ken \\',
+        '  node "$1/proxy.js" "$1/px.port" & PX=$!',
+        'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/px.port" ] && break; sleep 0.3; done',
+        'P="$(cat "$1/px.port")"',
+        'echo "port=$P"',
+        'echo "health=$(curl -sS "http://127.0.0.1:$P/__health")"',
+        'echo "unauth=$(curl -sS -o /dev/null -w %{http_code} -X POST "http://127.0.0.1:$P/v1/chat/completions")"',
+        'echo "auth=$(curl -sS -o /dev/null -w %{http_code} -X POST -H "Authorization: Bearer t0ken" "http://127.0.0.1:$P/v1/chat/completions")"',
+        'echo "wrong=$(curl -sS -o /dev/null -w %{http_code} -X POST -H "Authorization: Bearer nope" "http://127.0.0.1:$P/v1/chat/completions")"',
+        'node "$1/deadport.js" "$1/dead.port"',
+        'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/dead.port" ] && break; sleep 0.3; done',
+        'DEAD="$(cat "$1/dead.port")"',
+        'REVIEW_OPENAI_BASE_URL="http://127.0.0.1:$DEAD/v1" \\',
+        '  REVIEW_OPENAI_API_KEY=k QWEN_PROXY_NONCE=n0nce PROXY_TOKEN=t0ken \\',
+        '  node "$1/proxy.js" "$1/px2.port" & PX2=$!',
+        'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/px2.port" ] && break; sleep 0.3; done',
+        'P2="$(cat "$1/px2.port")"',
+        'echo "dead=$(curl -sS -o /dev/null -w %{http_code} -X POST -H "Authorization: Bearer t0ken" "http://127.0.0.1:$P2/v1/chat/completions")"',
+        'echo "dead2=$(curl -sS -o /dev/null -w %{http_code} -X POST -H "Authorization: Bearer t0ken" "http://127.0.0.1:$P2/v1/chat/completions")"',
+        'kill $UP $PX $PX2 2>/dev/null',
+      ].join('\n');
+      const out = spawnSync('bash', ['-c', driver, '_', dir], {
+        encoding: 'utf8',
+        timeout: 60000,
+      }).stdout;
+      // An OS-chosen port, identity proven by the nonce, and bearer-token
+      // gate rejecting unauthenticated callers.
+      expect(out).toMatch(/port=\d+/);
+      expect(out).toContain('health=n0nce');
+      expect(out).toContain('unauth=401');
+      expect(out).toContain('auth=200');
+      // The gate exists for the wrong-token case: a prefix match would let
+      // any 'Bearer ...' caller spend the real key.
+      expect(out).toContain('wrong=401');
+      // A dead upstream is a 502, not a crashed proxy: the outer catch must
+      // clear the hoisted timer without a ReferenceError and survive to serve
+      // the next call, or qwen's next completion hangs and the run maps the
+      // infrastructure fault to a false fail verdict.
+      expect(out).toContain('dead=502');
+      expect(out).toContain('dead2=502');
+    } finally {
+      rmSync(dir, { recursive: true, force: true });
+    }
+  });
+
+  // Same regression as the verify lane, asserted here because this PR carries
+  // the proxy across to tmux: the watchdog is idle (refreshed per chunk) and a
+  // mid-body stall terminates the response instead of stranding the client.
+  it('treats the tmux proxy watchdog as idle and ends a stalled response', () => {
+    const runStep = stepIn('tmux-testing', 'Run tmux real-user testing');
+    const proxy = runStep.match(/<<'NODE'\n([\s\S]*?)\n\s*NODE\n/)?.[1];
+    expect(proxy).toBeTruthy();
+    const out = runProxyWatchdogTest(proxy);
+    expect(out).toContain('chunks=20');
+    expect(out).toContain('stall_exit=18');
+  });
+
+  // PR lifecycle scripts run before the agent and can plant a
+  // tmp/-tmux-/ directory whose report.md and transcript the
+  // collector would hand to the publisher.
+  it('sweeps planted tmux artifacts before the agent starts', () => {
+    const runStep = stepIn('tmux-testing', 'Run tmux real-user testing');
+    const sweep =
+      "find tmp -maxdepth 2 -type d -name '*-tmux-*' -exec rm -rf {} +";
+    const sweepAt = runStep.indexOf(sweep);
+    expect(sweepAt).toBeGreaterThan(-1);
+    // Before the proxy and the agent launch, after the build.
+    expect(sweepAt).toBeLessThan(runStep.indexOf('start_openai_proxy'));
+    // The sweep must not descend through a PR-planted `tmp` symlink: the
+    // same root-owned escape the .qwen cleanup guards against.
+    const guardAt = runStep.indexOf('if [ -L tmp ]; then');
+    expect(guardAt).toBeGreaterThan(-1);
+    expect(guardAt).toBeLessThan(sweepAt);
+  });
+
+  // The global install must not read the previous PR's .npmrc: a --registry
+  // flag does not override script-shell or hooks.
+  it('runs the tmux global install away from the checked-out tree', () => {
+    expect(stepIn('tmux-testing', 'Install tmux runner tools')).toContain(
+      '(cd "${RUNNER_TEMP:?}" && npm install -g',
+    );
+  });
+
+  // Cleanup must not descend through a PR-writable parent. Both the
+  // pre-checkout step and the end-of-job step need the guard: the
+  // pre-checkout site runs as root against the previous run's PR-written
+  // tree before actions/checkout cleans anything.
+  it('unlinks tmux-lane symlinks instead of globbing through them', () => {
+    const preCheckout = stepIn('tmux-testing', 'Clean stale review worktrees');
+    expect(preCheckout).toContain('[ -L .qwen ] && rm -f .qwen');
+    expect(preCheckout).toContain('if [ -L .qwen/tmp ]; then');
+    const endOfJob = stepIn('tmux-testing', 'Clean up runner workspace');
+    expect(endOfJob).toContain('[ -L .qwen ] && rm -f .qwen');
+    expect(endOfJob).toContain('if [ -L .qwen/tmp ]; then');
+  });
+
+  // cp -r copies symlinks as symlinks, but actions/upload-artifact follows
+  // them — a node-planted link would exfiltrate its target into the artifact
+  // and then into the public PR comment.
+  it('strips symlinks from collected tmux artifacts', () => {
+    const runStep = stepIn('tmux-testing', 'Run tmux real-user testing');
+    const collect = runStep.indexOf('cp -r {} "$RUNNER_TEMP/tmux-results/"');
+    expect(collect).toBeGreaterThan(-1);
+    const strip = runStep.indexOf(
+      'find "$RUNNER_TEMP/tmux-results" -type l -delete',
+    );
+    expect(strip).toBeGreaterThan(collect);
+  });
+
+  // Escaping inflates & < > by 4-5 bytes each, so a raw-side cap can push
+  // the assembled comment past GitHub's 65,536-char limit and 422 the post.
+  it('caps the tmux comment after escaping, on a character boundary', () => {
+    const publish = stepIn('publish-tmux', 'Post tmux result comment');
+    const escFirst = publish.indexOf('html_escape > "$esc_file"');
+    expect(escFirst).toBeGreaterThan(-1);
+    expect(publish).toContain('TextDecoder');
+    expect(publish).not.toContain('head -c "$max" "$file" | tr -d');
+
+    // Execute it: dense metacharacter content must stay under the cap and
+    // remain valid UTF-8.
+    const script = publish
+      .match(/run: \|-\n([\s\S]*)$/)?.[1]
+      .replace(/^ {10}/gm, '');
+    const helpers = script.slice(
+      script.indexOf('html_escape()'),
+      script.indexOf('if [ "${TMUX_RESULT:-}"'),
+    );
+    const dir = mkdtempSync(join(tmpdir(), 'tmux-emit-'));
+    try {
+      const dense = join(dir, 'dense.log');
+      writeFileSync(dense, '>&'.repeat(7300));
+      const utf8 = join(dir, 'utf8.log');
+      // One ASCII byte of padding so the cut lands inside a 3-byte char.
+      writeFileSync(utf8, `x${'验证证据链路测试'.repeat(8000)}`);
+      const emit = (file) => {
+        const proc = spawnSync(
+          'bash',
+          ['-c', `${helpers}\nemit_block 'Log' "$1" 20000`, '_', file],
+          { encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 },
+        );
+        expect(proc.status).toBe(0);
+        return proc.stdout;
+      };
+      const capped = emit(dense);
+      expect(Buffer.byteLength(capped)).toBeLessThan(65536);
+      expect(capped).toContain('truncated');
+      const cut = emit(utf8);
+      expect(cut).not.toContain('\ufffd');
+    } finally {
+      rmSync(dir, { recursive: true, force: true });
+    }
+  });
+
+  // A detached lifecycle child can outlive the build step and the one-shot
+  // sweep, re-planting artifacts or scanning localhost for the model proxy.
+  // The verify lane kills the build user's processes before any cleanup; the
+  // tmux lane runs the same untrusted code on the same pool and must too.
+  it('kills surviving build-user processes before the tmux agent starts', () => {
+    const runStep = stepIn('tmux-testing', 'Run tmux real-user testing');
+    expect(runStep).toContain('pkill -KILL -u node');
+    expect(runStep).toContain(
+      'Processes owned by the build user survived; refusing to start the agent.',
+    );
+    // Before the sweep and the proxy: the cleanup must not race a live
+    // process, and no leftover child may be alive when the proxy binds.
+    const killAt = runStep.indexOf('pkill -KILL -u node');
+    expect(killAt).toBeGreaterThan(-1);
+    expect(killAt).toBeLessThan(
+      runStep.indexOf("find tmp -maxdepth 2 -type d -name '*-tmux-*'"),
+    );
+    expect(killAt).toBeLessThan(runStep.indexOf('start_openai_proxy'));
+  });
+
+  // publish-verify bounds itself so a hung gh call cannot hold a hosted
+  // runner for the 360-minute default; publish-tmux posts the same way.
+  it('bounds the publish-tmux job with a timeout', () => {
+    const publish = job('publish-tmux');
+    expect(publish).toMatch(/timeout-minutes: \d+/);
+    const minutes = Number(publish.match(/timeout-minutes: (\d+)/)?.[1]);
+    expect(minutes).toBeGreaterThan(0);
+    expect(minutes).toBeLessThanOrEqual(30);
+  });
+
+  // A per-RUN concurrency group (not per-PR) stops two publish-tmux jobs in
+  // the same run racing the post, while never letting a newer run cancel a
+  // completed run's pending publisher and drop its report. Parity with
+  // publish-verify.
+  it('serializes publish-tmux with a per-run concurrency group', () => {
+    const publish = job('publish-tmux');
+    expect(publish).toContain('concurrency:');
+    expect(publish).toContain('publish-tmux-{1}');
+    expect(publish).toContain('cancel-in-progress: false');
+  });
+
+  // The publisher must select the agent's report by TYPE and anchored PATH,
+  // not a loose `-name report.md | head -1`: a planted DIRECTORY named
+  // report.md that sorted ahead of the real one won the old predicate, and
+  // emit_block's [ -f ] guard then dropped the report silently while the
+  // non-empty REPORT string suppressed the missing-artifact note. Parity
+  // with the verify lane's predicate.
+  it('selects tmux artifacts by type and path, ignoring planted directories', () => {
+    const publish = stepIn('publish-tmux', 'Post tmux result comment');
+    expect(publish).toContain(
+      "find tmux-results -mindepth 2 -type f -path '*-tmux-*/report.md' 2>/dev/null | sort | head -1",
+    );
+    expect(publish).toContain(
+      "find tmux-results -mindepth 2 -type f -path '*-tmux-*/tmux-readable-full.log' 2>/dev/null | sort | head -1",
+    );
+
+    const dir = mkdtempSync(join(tmpdir(), 'tmux-select-'));
+    try {
+      // A planted directory named report.md that sorts FIRST.
+      mkdirSync(join(dir, 'tmux-results/AAA-planted-tmux-0/report.md'), {
+        recursive: true,
+      });
+      mkdirSync(join(dir, 'tmux-results/real-tmux-1'), { recursive: true });
+      writeFileSync(
+        join(dir, 'tmux-results/real-tmux-1/report.md'),
+        '## real report\n',
+      );
+      const out = spawnSync(
+        'bash',
+        [
+          '-c',
+          "find tmux-results -mindepth 2 -type f -path '*-tmux-*/report.md' 2>/dev/null | sort | head -1",
+        ],
+        { encoding: 'utf8', cwd: dir },
+      ).stdout.trim();
+      expect(out).toBe('tmux-results/real-tmux-1/report.md');
+    } finally {
+      rmSync(dir, { recursive: true, force: true });
+    }
+  });
+
+  // Dedup must PATCH only a BOT-OWNED comment STARTING with the marker.
+  // contains() with no author filter let a human reviewer who quoted the
+  // marker have their comment overwritten by the bot's PAT (#7723). Fail
+  // closed on identity, same as publish-verify.
+  it('dedups the tmux comment on a bot-owned prefix match, fail-closed', () => {
+    const publish = stepIn('publish-tmux', 'Post tmux result comment');
+    expect(publish).toContain('startswith("")');
+    expect(publish).toContain('.user.login == $bot');
+    expect(publish).not.toContain('contains("")');
+    expect(publish).toContain("gh api user --jq '.login'");
+  });
+
+  // GitHub 422s a comment over 65,536 chars and posts nothing. The invariant
+  // is the SUM of the two block caps plus the envelope, not any single block:
+  // a single-block assertion passes for any cap under ~65,000, so bumping the
+  // transcript cap from 30000 to 60000 would 422 the post undetected.
+  it('keeps the sum of the tmux block caps under the comment limit', () => {
+    const publish = stepIn('publish-tmux', 'Post tmux result comment');
+    const reportCap = Number(
+      publish.match(/emit_block 'E2E test report' "\$REPORT" (\d+)/)?.[1],
+    );
+    const transcriptCap = Number(
+      publish.match(
+        /emit_block 'Full tmux transcript' "\$TRANSCRIPT" (\d+)/,
+      )?.[1],
+    );
+    expect(reportCap).toBeGreaterThan(0);
+    expect(transcriptCap).toBeGreaterThan(0);
+    const envelope = 4096; // verdict header, description, markers, signature
+    expect(reportCap + transcriptCap + envelope).toBeLessThan(65536);
   });
 });