From 9f1652c690f7b2d041ca7d47033a0e61d449245a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:29:26 +0000 Subject: [PATCH] fix(agents,scripts): PIPESTATUS/pipefail are exit-code-safe only when the downstream reads to EOF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The os-dev exit-code discipline listed `set -o pipefail` and `${PIPESTATUS[0]}` as safe capture forms. Measured on a refusing gate: `| head -N` closes the read end early, the producer takes EPIPE/SIGPIPE and exits 0 — the pipe changes the producer's exit code, so PIPESTATUS reports that 0 faithfully and pipefail reads the same changed status. Only redirect-then-capture is immune. Downgrade both forms to EOF-conditional, name `| head -N` as the measured counterexample, and state the corollary: any `cmd | head` followed by an exit-code read is hit, not just gates. Same PR, second copy of the same wrong fact: the i18n gate's refusal banner warned against `| tail -4` (which reads 1, correctly, via PIPESTATUS) and left `| head` — the shape that actually reads green — unnamed. The advisory now names the true hazard and the safe capture. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de --- .claude/agents/os-dev.md | 22 +++++++++++----------- scripts/check-i18n-bundles.mjs | 6 ++++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index d0442b61b5..a712a14c2b 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -137,8 +137,8 @@ pin 要防的失效 —— 而不是回退到本行。 --> double** —— 不新增要 pin 的 double,不动台账。 - 匹配到零个脚本的 `pnpm --filter` 运行**以 0 退出**——什么都没跑,读上去却是通过;objectui 把 typecheck 拼作 `type-check`(连字符),拼错脚本名正好落进这个坑(拼对时依赖闭包没 - build 它本会转红,零匹配却静默变绿)。核对输出里确实回显了脚本名,或从 `PIPESTATUS` 读 - 退出码,⛔ 永不隔着管道 `tail` 下结论——ERR_PNPM 提示会被滚出视野。 + build 它本会转红,零匹配却静默变绿)。核对输出里确实回显了脚本名,退出码先重定向再捕 + 获,⛔ 永不隔着管道下结论——ERR_PNPM 会滚出视野,`| head` 连 `PIPESTATUS` 都改绿(见下节)。 - 浏览器验证:Chromium **已预装**——`PLAYWRIGHT_BROWSERS_PATH` 指向 `/opt/pw-browsers`,launch 传 `executablePath: '/opt/pw-browsers/chromium'`。⛔ 永不跑 `playwright install`(出口策略拦它),且 `cdn.playwright.dev` 的 403 不是「没有浏览器」证据——下载被拦不证明产物缺席,先找产物。 @@ -185,15 +185,15 @@ pin 要防的失效 —— 而不是回退到本行。 --> 且没人会察觉 —— 迟到的 commit 挪动的恰恰是过期的 **ratchet** 读数。复核后任何一次 push,都在新 head 上重跑并集 —— 至少 ratchet 族 —— **然后**才更新报告或 PR 正文。 -**门禁结果的读法:退出码在任何管道之前捕获,报告里引门禁自己印的判定行。** `EXIT=$?` 跟 -在 `cmd 2>&1 | tail -40` 之后,读到的是 **`tail` 的**状态:`tail` 基本永不失败,绿门禁与红门禁读出 -来都是 `0`(实测:一次 typecheck 印着 `Exit status 2`,旁边的 `EXIT` 行写 `0`)。这不只是不可 -靠,是**不可证伪**:对两种结局返回同一个值,重跑也翻不出来,却在报告里读作一次测量。三 -种安全写法任选:先重定向再捕获(`cmd > /tmp/out 2>&1; EXIT=$?; tail -40 /tmp/out`)·`set -o pipefail`· -`${PIPESTATUS[0]}`。另一半在报告侧:**引用某个门禁结果时,点名它自己印出的判定行**,永不引 -裸 `$?` —— 判定行由门禁写,`$?` 由你的管道写。⛔ 别等机械强制:陷阱在 agent 的 shell 用法 -里,没有任何受版本控制的产物看得见它,已判定不可机械化 —— 这两条纪律就是全部的守 -卫。 +**门禁结果的读法:退出码在任何管道之前捕获,报告里引门禁自己印的判定行。** 免疫写法 +只有一种:先重定向再捕获(`cmd > /tmp/out 2>&1; EXIT=$?; tail -40 /tmp/out`)。两个实测管道假绿, +同样**不可证伪**(红绿都回同一个 0,重跑翻不出来):① `EXIT=$?` 跟在 `cmd 2>&1 | tail -40` 之 +后读到的是 **`tail` 的**状态;② `set -o pipefail` 与 `${PIPESTATUS[0]}` 仅当下游读到 EOF 才安全, +`| head -N` 是实测反例:读满即关读端,生产者吃 EPIPE/SIGPIPE 以 0 退出 —— 是管道改了生产者 +的退出码,PIPESTATUS 如实上报,`pipefail` 读的也是同一个被改的状态。此陷阱不限于门禁:任何 +`cmd | head` 之后读退出码都中招(`git grep`、`node` 皆然);读输出无事,读退出码即假绿。另一半 +在报告侧:**引用门禁结果时,点名它自己印出的判定行**,永不引裸 `$?` —— 判定行由门禁 +写,`$?` 由你的管道写。⛔ 别等机械强制:陷阱在 shell 用法里,已判定不可机械化。 **两类「跑了却没测到」,都读作 NOT MEASURED,不读作绿、也不读作红。** ① 包的 `typecheck` 可 能 `exclude` 掉 `**/*.test.ts` —— 于是「typecheck 干净」是一句真话,却对你新写的测试文件一个 diff --git a/scripts/check-i18n-bundles.mjs b/scripts/check-i18n-bundles.mjs index 4d912a3aab..11982f7442 100644 --- a/scripts/check-i18n-bundles.mjs +++ b/scripts/check-i18n-bundles.mjs @@ -990,8 +990,10 @@ function reportPrerequisiteNotMet(headline, detail, options = {}) { `\n\n Fix: ${fix}\n` + alsoFix.map((l) => ` ${l}\n`).join('') + `\n${nothingChecked}\n` + - ` (Exit code 1 — but piping this gate reports the PIPE's status, so\n` + - ` \`pnpm check:i18n | tail -4\` reads green either way. Use \`echo "EXIT=$?"\`.)`, + ` (Exit code 1 — capture it BEFORE any pipe: \`pnpm check:i18n > /tmp/i18n.log 2>&1; echo "EXIT=$?"\`.\n` + + ` Piped, \`$?\` is the pipe's status, and \`| head -N\` turns even \`\${PIPESTATUS[0]}\`/\`pipefail\` green:\n` + + ` \`head\` closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the\n` + + ` exit code itself. \`| tail\` reads to EOF; \`\${PIPESTATUS[0]}\` after it is the true status.)`, ); process.exit(1); }