Skip to content

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings
Aug 31, 2026
Merged

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner#13828
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13429

Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.

⚠️ Read this first: the prescribed mechanism is factually wrong, and this PR does not propagate it

Zone 1 binds me to name | head -N and the EPIPE mechanism, to name the safe capture, and to keep | tail identified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:

Piped, $? is the pipe's status, and | head -N turns even ${PIPESTATUS[0]}/pipefail green: head closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the exit code itself.

Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:

node scripts/check-test-completeness.mjs -> 3 (no pipe)
node ... 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
node ... 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3

| head did not turn ${PIPESTATUS[0]} or pipefail green. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes, pnpm exec in front, a bash wrapper): every one preserved its non-zero code through | head -1.

And EPIPE genuinely happens — this is not "head read everything":

node -e 'process.stdout.on("error", e => process.stderr.write("PRODUCER-SAW: "+e.code+"\n")); ...exit(7)' | head -1
PRODUCER-SAW: EPIPE
${PIPESTATUS[0]} = 7

The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives | head intact.

What is actually true, and what all nine sites now say:

  • The false green is bare $? after any pipe — it is the LAST command's status, and head/tail both essentially never fail. It is not a property of one shape, and no choice of shape repairs it.
  • ${PIPESTATUS[0]}/pipefail do recover the gate's own code. | tail reads to EOF and forwards it.
  • | head -N closes the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141: seq 1 100000000 | head -1 reports ${PIPESTATUS[0]} = 141. That is a false red, the opposite direction from the one claimed.

Because the reference site would otherwise diverge from the other eight and keep the false claim, scripts/check-i18n-bundles.mjs is corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.

The measurement is anchored where the next author will look: a block comment on reportPrerequisiteNotMet, with an explicit "do NOT write that | head turns PIPESTATUS/pipefail green" and why.

Census instrument

Verbatim, the command used to find the sites:

git grep -n "reads green either way"
git grep -n "the PIPE's status" -- 'scripts/**' 'packages/**'

The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:

git grep -n "no pipe shape repairs it"

which now returns all nine sites across the six files.

Inheritor count: measured 45, not 44 and not 43

Derivation, verbatim:

git grep -lE "^import .*from '.*import-prerequisite\.mjs'"

45 files. Both circulating numbers are wrong, in opposite directions:

  • Triage's 44 is git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one — scripts/pm/dispatch-gates.mjs — only mentions the filename in a comment and imports nothing. So under scripts/** the real importer count is 42.
  • The dispatch order's corrected 43 fixes the self-reference but keeps the scripts/** pathspec, which is itself too narrow: three real importers live in packages/lint/scripts/ and no scripts/** glob can see them.

42 + 3 = 45. Verified live: the corrected banner rendered from packages/lint/scripts/check-doc-formula-expressions.mjs during this PR's gate run.

A2.4 inherit-vs-shadow split: the sets are disjoint

# importers.txt : the 45 paths from the derivation above, sorted
# shadows.txt : git grep -ln "PREREQUISITE NOT MET" , sorted
comm -12 importers.txt shadows.txt -> empty

Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (check-i18n-coverage.mjs, check-half-states.mjs, ci-failure.mjs, check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.

(scripts/check-dual-build-cjs-loads.mjs also carries a local PREREQUISITE NOT MET banner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)

Sites the card's instrument missed

Two, both surfaced by the mechanism census:

  1. scripts/pm/ci-failure.mjs:170 — a module-header comment, Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.
  2. packages/cli/scripts/check-app-nav-i18n.mjs:620 — true but incomplete (prescribes echo "EXIT=$?" without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding] packages/cli/scripts/check-app-nav-i18n.mjs prerequisite banner says echo "EXIT=$?" but never says BEFORE any pipe, and names no shape #13825.

Judged out of class and deliberately untouched: three .github/workflows/*.yml comments that say cmd | tail reports the pipe's status. Each is scoped to bare $?, where it is true, and each sits beside code that already does redirect-then-capture.

check:ratchet-remedy-authority (A2.5)

The dispatch was right that no path derivation names it. node scripts/pm/dispatch-gates.mjs derived 23 families for this diff and check:ratchet-remedy-authority is not among them. Run explicitly anyway — its own verdict line:

OK check-ratchet-remedy-authority: 176 scripts swept (scripts/*.{mjs,mts}); 11 mark the
expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it down outright, 160 hand out no
ratchet-expanding remedy.

Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.

Tests

All gate results below are from 8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.

Self-tests — the constraint the card names (each must stay green, no exit code may change):

gate--self-test
scripts/import-prerequisite.mjs0 — 41 cases (was 36; 5 added)
scripts/check-i18n-coverage.mjs0
scripts/check-i18n-bundles.mjs0
scripts/check-test-completeness.mjs0
scripts/pm/check-half-states.mjs0 — 1826 cases
scripts/pm/ci-failure.mjs0

Exit codes unchanged, spot-checked on the live refusal path: node scripts/check-test-completeness.mjs answered 3 before the edit and 3 after.

New pins.reportPrerequisiteNotMet is split into a pure text builder plus a thin printer (no new export, no caller change, process.exit(1) untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green; | tail kept as the forwarding shape; | head -N plus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats ${PIPESTATUS[0]}/pipefail.

Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a git hash-object move (55b7512b -> b511968f); no build/dist leg exists here, these .mjs files are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to 55b7512b, git diff HEAD empty, git status --porcelain empty, self-test back to 41/41 green.

Derived gate union — 25 commands, all green.node scripts/pm/dispatch-gates.mjs (no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, plus check:ratchet-remedy-authority and check:nul-bytes.

Two of them are not locally runnable in their bare form and were run in the shape CI uses: check:partof-closing-keyword needs PR_BODY (its package.json script is --self-test only, which is what CI runs), and node scripts/check-test-completeness.mjs needs a real CI log (CI runs it inside Test Core). Three gates first exited 1 with a PREREQUISITE NOT MET refusal because this worktree had no node_modules yet — read as NOT MEASURED, not red; all three went green after pnpm install. check:i18n and check:i18n-coverage each refused first for an unbuilt closure and both went green once the closure they named was built.

ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide pnpm lint:

pnpm exec eslint --no-inline-config --format json scripts/
-> exit 0 · 204 files · 0 errors · 0 warnings
  1. The population is eslint's own — it resolved 204 files under scripts/ from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.
  2. The count is read from --format json, not from console text.
  3. Config invariance: this repo runs one eslint.config.mjs which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for any file — stated at eslint.config.mjs:327-329 and measured there with a positive control. So no verdict on an untouched file can move because of this diff.

Control bytes:check:nul-bytes exit 0 (7594 files scanned), plus a direct grep -naP control-character scan over the six changed files — clean.

Changeset: skip-changeset, and why this contradicts the dispatch order

The dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:

  • Nothing here ships. The root package is private: true with no files, and @objectstack/lint — the only package with importers in this diff's blast radius — publishes ["dist","README.md","CHANGELOG.md"], so packages/lint/scripts/ is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.
  • An empty-frontmatter changeset is mechanically rejected: scripts/check-empty-changeset.mjs refuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input to changesets/action and can stall a release; the label produces no input at all).

So skip-changeset is the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are root scripts/*.mjs rather than scripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.


Generated by Claude Code

…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 4cc99bcAug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13429-pipe-shape-advisory-siblings branch August 31, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner by claude[bot] · Pull Request #13828 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings
Aug 31, 2026
Merged

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner#13828
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13429

Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.

⚠️ Read this first: the prescribed mechanism is factually wrong, and this PR does not propagate it

Zone 1 binds me to name | head -N and the EPIPE mechanism, to name the safe capture, and to keep | tail identified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:

Piped, $? is the pipe's status, and | head -N turns even ${PIPESTATUS[0]}/pipefail green: head closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the exit code itself.

Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:

node scripts/check-test-completeness.mjs -> 3 (no pipe)
node ... 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
node ... 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3

| head did not turn ${PIPESTATUS[0]} or pipefail green. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes, pnpm exec in front, a bash wrapper): every one preserved its non-zero code through | head -1.

And EPIPE genuinely happens — this is not "head read everything":

node -e 'process.stdout.on("error", e => process.stderr.write("PRODUCER-SAW: "+e.code+"\n")); ...exit(7)' | head -1
PRODUCER-SAW: EPIPE
${PIPESTATUS[0]} = 7

The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives | head intact.

What is actually true, and what all nine sites now say:

  • The false green is bare $? after any pipe — it is the LAST command's status, and head/tail both essentially never fail. It is not a property of one shape, and no choice of shape repairs it.
  • ${PIPESTATUS[0]}/pipefail do recover the gate's own code. | tail reads to EOF and forwards it.
  • | head -N closes the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141: seq 1 100000000 | head -1 reports ${PIPESTATUS[0]} = 141. That is a false red, the opposite direction from the one claimed.

Because the reference site would otherwise diverge from the other eight and keep the false claim, scripts/check-i18n-bundles.mjs is corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.

The measurement is anchored where the next author will look: a block comment on reportPrerequisiteNotMet, with an explicit "do NOT write that | head turns PIPESTATUS/pipefail green" and why.

Census instrument

Verbatim, the command used to find the sites:

git grep -n "reads green either way"
git grep -n "the PIPE's status" -- 'scripts/**' 'packages/**'

The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:

git grep -n "no pipe shape repairs it"

which now returns all nine sites across the six files.

Inheritor count: measured 45, not 44 and not 43

Derivation, verbatim:

git grep -lE "^import .*from '.*import-prerequisite\.mjs'"

45 files. Both circulating numbers are wrong, in opposite directions:

  • Triage's 44 is git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one — scripts/pm/dispatch-gates.mjs — only mentions the filename in a comment and imports nothing. So under scripts/** the real importer count is 42.
  • The dispatch order's corrected 43 fixes the self-reference but keeps the scripts/** pathspec, which is itself too narrow: three real importers live in packages/lint/scripts/ and no scripts/** glob can see them.

42 + 3 = 45. Verified live: the corrected banner rendered from packages/lint/scripts/check-doc-formula-expressions.mjs during this PR's gate run.

A2.4 inherit-vs-shadow split: the sets are disjoint

# importers.txt : the 45 paths from the derivation above, sorted
# shadows.txt : git grep -ln "PREREQUISITE NOT MET" , sorted
comm -12 importers.txt shadows.txt -> empty

Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (check-i18n-coverage.mjs, check-half-states.mjs, ci-failure.mjs, check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.

(scripts/check-dual-build-cjs-loads.mjs also carries a local PREREQUISITE NOT MET banner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)

Sites the card's instrument missed

Two, both surfaced by the mechanism census:

  1. scripts/pm/ci-failure.mjs:170 — a module-header comment, Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.
  2. packages/cli/scripts/check-app-nav-i18n.mjs:620 — true but incomplete (prescribes echo "EXIT=$?" without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding] packages/cli/scripts/check-app-nav-i18n.mjs prerequisite banner says echo "EXIT=$?" but never says BEFORE any pipe, and names no shape #13825.

Judged out of class and deliberately untouched: three .github/workflows/*.yml comments that say cmd | tail reports the pipe's status. Each is scoped to bare $?, where it is true, and each sits beside code that already does redirect-then-capture.

check:ratchet-remedy-authority (A2.5)

The dispatch was right that no path derivation names it. node scripts/pm/dispatch-gates.mjs derived 23 families for this diff and check:ratchet-remedy-authority is not among them. Run explicitly anyway — its own verdict line:

OK check-ratchet-remedy-authority: 176 scripts swept (scripts/*.{mjs,mts}); 11 mark the
expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it down outright, 160 hand out no
ratchet-expanding remedy.

Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.

Tests

All gate results below are from 8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.

Self-tests — the constraint the card names (each must stay green, no exit code may change):

gate--self-test
scripts/import-prerequisite.mjs0 — 41 cases (was 36; 5 added)
scripts/check-i18n-coverage.mjs0
scripts/check-i18n-bundles.mjs0
scripts/check-test-completeness.mjs0
scripts/pm/check-half-states.mjs0 — 1826 cases
scripts/pm/ci-failure.mjs0

Exit codes unchanged, spot-checked on the live refusal path: node scripts/check-test-completeness.mjs answered 3 before the edit and 3 after.

New pins.reportPrerequisiteNotMet is split into a pure text builder plus a thin printer (no new export, no caller change, process.exit(1) untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green; | tail kept as the forwarding shape; | head -N plus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats ${PIPESTATUS[0]}/pipefail.

Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a git hash-object move (55b7512b -> b511968f); no build/dist leg exists here, these .mjs files are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to 55b7512b, git diff HEAD empty, git status --porcelain empty, self-test back to 41/41 green.

Derived gate union — 25 commands, all green.node scripts/pm/dispatch-gates.mjs (no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, plus check:ratchet-remedy-authority and check:nul-bytes.

Two of them are not locally runnable in their bare form and were run in the shape CI uses: check:partof-closing-keyword needs PR_BODY (its package.json script is --self-test only, which is what CI runs), and node scripts/check-test-completeness.mjs needs a real CI log (CI runs it inside Test Core). Three gates first exited 1 with a PREREQUISITE NOT MET refusal because this worktree had no node_modules yet — read as NOT MEASURED, not red; all three went green after pnpm install. check:i18n and check:i18n-coverage each refused first for an unbuilt closure and both went green once the closure they named was built.

ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide pnpm lint:

pnpm exec eslint --no-inline-config --format json scripts/
-> exit 0 · 204 files · 0 errors · 0 warnings
  1. The population is eslint's own — it resolved 204 files under scripts/ from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.
  2. The count is read from --format json, not from console text.
  3. Config invariance: this repo runs one eslint.config.mjs which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for any file — stated at eslint.config.mjs:327-329 and measured there with a positive control. So no verdict on an untouched file can move because of this diff.

Control bytes:check:nul-bytes exit 0 (7594 files scanned), plus a direct grep -naP control-character scan over the six changed files — clean.

Changeset: skip-changeset, and why this contradicts the dispatch order

The dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:

  • Nothing here ships. The root package is private: true with no files, and @objectstack/lint — the only package with importers in this diff's blast radius — publishes ["dist","README.md","CHANGELOG.md"], so packages/lint/scripts/ is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.
  • An empty-frontmatter changeset is mechanically rejected: scripts/check-empty-changeset.mjs refuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input to changesets/action and can stall a release; the label produces no input at all).

So skip-changeset is the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are root scripts/*.mjs rather than scripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.


Generated by Claude Code

…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 4cc99bcAug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13429-pipe-shape-advisory-siblings branch August 31, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner by claude[bot] · Pull Request #13828 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings
Aug 31, 2026
Merged

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner#13828
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13429

Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.

⚠️ Read this first: the prescribed mechanism is factually wrong, and this PR does not propagate it

Zone 1 binds me to name | head -N and the EPIPE mechanism, to name the safe capture, and to keep | tail identified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:

Piped, $? is the pipe's status, and | head -N turns even ${PIPESTATUS[0]}/pipefail green: head closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the exit code itself.

Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:

node scripts/check-test-completeness.mjs -> 3 (no pipe)
node ... 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
node ... 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3

| head did not turn ${PIPESTATUS[0]} or pipefail green. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes, pnpm exec in front, a bash wrapper): every one preserved its non-zero code through | head -1.

And EPIPE genuinely happens — this is not "head read everything":

node -e 'process.stdout.on("error", e => process.stderr.write("PRODUCER-SAW: "+e.code+"\n")); ...exit(7)' | head -1
PRODUCER-SAW: EPIPE
${PIPESTATUS[0]} = 7

The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives | head intact.

What is actually true, and what all nine sites now say:

  • The false green is bare $? after any pipe — it is the LAST command's status, and head/tail both essentially never fail. It is not a property of one shape, and no choice of shape repairs it.
  • ${PIPESTATUS[0]}/pipefail do recover the gate's own code. | tail reads to EOF and forwards it.
  • | head -N closes the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141: seq 1 100000000 | head -1 reports ${PIPESTATUS[0]} = 141. That is a false red, the opposite direction from the one claimed.

Because the reference site would otherwise diverge from the other eight and keep the false claim, scripts/check-i18n-bundles.mjs is corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.

The measurement is anchored where the next author will look: a block comment on reportPrerequisiteNotMet, with an explicit "do NOT write that | head turns PIPESTATUS/pipefail green" and why.

Census instrument

Verbatim, the command used to find the sites:

git grep -n "reads green either way"
git grep -n "the PIPE's status" -- 'scripts/**' 'packages/**'

The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:

git grep -n "no pipe shape repairs it"

which now returns all nine sites across the six files.

Inheritor count: measured 45, not 44 and not 43

Derivation, verbatim:

git grep -lE "^import .*from '.*import-prerequisite\.mjs'"

45 files. Both circulating numbers are wrong, in opposite directions:

  • Triage's 44 is git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one — scripts/pm/dispatch-gates.mjs — only mentions the filename in a comment and imports nothing. So under scripts/** the real importer count is 42.
  • The dispatch order's corrected 43 fixes the self-reference but keeps the scripts/** pathspec, which is itself too narrow: three real importers live in packages/lint/scripts/ and no scripts/** glob can see them.

42 + 3 = 45. Verified live: the corrected banner rendered from packages/lint/scripts/check-doc-formula-expressions.mjs during this PR's gate run.

A2.4 inherit-vs-shadow split: the sets are disjoint

# importers.txt : the 45 paths from the derivation above, sorted
# shadows.txt : git grep -ln "PREREQUISITE NOT MET" , sorted
comm -12 importers.txt shadows.txt -> empty

Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (check-i18n-coverage.mjs, check-half-states.mjs, ci-failure.mjs, check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.

(scripts/check-dual-build-cjs-loads.mjs also carries a local PREREQUISITE NOT MET banner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)

Sites the card's instrument missed

Two, both surfaced by the mechanism census:

  1. scripts/pm/ci-failure.mjs:170 — a module-header comment, Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.
  2. packages/cli/scripts/check-app-nav-i18n.mjs:620 — true but incomplete (prescribes echo "EXIT=$?" without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding] packages/cli/scripts/check-app-nav-i18n.mjs prerequisite banner says echo "EXIT=$?" but never says BEFORE any pipe, and names no shape #13825.

Judged out of class and deliberately untouched: three .github/workflows/*.yml comments that say cmd | tail reports the pipe's status. Each is scoped to bare $?, where it is true, and each sits beside code that already does redirect-then-capture.

check:ratchet-remedy-authority (A2.5)

The dispatch was right that no path derivation names it. node scripts/pm/dispatch-gates.mjs derived 23 families for this diff and check:ratchet-remedy-authority is not among them. Run explicitly anyway — its own verdict line:

OK check-ratchet-remedy-authority: 176 scripts swept (scripts/*.{mjs,mts}); 11 mark the
expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it down outright, 160 hand out no
ratchet-expanding remedy.

Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.

Tests

All gate results below are from 8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.

Self-tests — the constraint the card names (each must stay green, no exit code may change):

gate--self-test
scripts/import-prerequisite.mjs0 — 41 cases (was 36; 5 added)
scripts/check-i18n-coverage.mjs0
scripts/check-i18n-bundles.mjs0
scripts/check-test-completeness.mjs0
scripts/pm/check-half-states.mjs0 — 1826 cases
scripts/pm/ci-failure.mjs0

Exit codes unchanged, spot-checked on the live refusal path: node scripts/check-test-completeness.mjs answered 3 before the edit and 3 after.

New pins.reportPrerequisiteNotMet is split into a pure text builder plus a thin printer (no new export, no caller change, process.exit(1) untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green; | tail kept as the forwarding shape; | head -N plus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats ${PIPESTATUS[0]}/pipefail.

Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a git hash-object move (55b7512b -> b511968f); no build/dist leg exists here, these .mjs files are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to 55b7512b, git diff HEAD empty, git status --porcelain empty, self-test back to 41/41 green.

Derived gate union — 25 commands, all green.node scripts/pm/dispatch-gates.mjs (no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, plus check:ratchet-remedy-authority and check:nul-bytes.

Two of them are not locally runnable in their bare form and were run in the shape CI uses: check:partof-closing-keyword needs PR_BODY (its package.json script is --self-test only, which is what CI runs), and node scripts/check-test-completeness.mjs needs a real CI log (CI runs it inside Test Core). Three gates first exited 1 with a PREREQUISITE NOT MET refusal because this worktree had no node_modules yet — read as NOT MEASURED, not red; all three went green after pnpm install. check:i18n and check:i18n-coverage each refused first for an unbuilt closure and both went green once the closure they named was built.

ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide pnpm lint:

pnpm exec eslint --no-inline-config --format json scripts/
-> exit 0 · 204 files · 0 errors · 0 warnings
  1. The population is eslint's own — it resolved 204 files under scripts/ from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.
  2. The count is read from --format json, not from console text.
  3. Config invariance: this repo runs one eslint.config.mjs which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for any file — stated at eslint.config.mjs:327-329 and measured there with a positive control. So no verdict on an untouched file can move because of this diff.

Control bytes:check:nul-bytes exit 0 (7594 files scanned), plus a direct grep -naP control-character scan over the six changed files — clean.

Changeset: skip-changeset, and why this contradicts the dispatch order

The dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:

  • Nothing here ships. The root package is private: true with no files, and @objectstack/lint — the only package with importers in this diff's blast radius — publishes ["dist","README.md","CHANGELOG.md"], so packages/lint/scripts/ is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.
  • An empty-frontmatter changeset is mechanically rejected: scripts/check-empty-changeset.mjs refuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input to changesets/action and can stall a release; the label produces no input at all).

So skip-changeset is the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are root scripts/*.mjs rather than scripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.


Generated by Claude Code

…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 4cc99bcAug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13429-pipe-shape-advisory-siblings branch August 31, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner by claude[bot] · Pull Request #13828 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings
Aug 31, 2026
Merged

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner#13828
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13429

Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.

⚠️ Read this first: the prescribed mechanism is factually wrong, and this PR does not propagate it

Zone 1 binds me to name | head -N and the EPIPE mechanism, to name the safe capture, and to keep | tail identified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:

Piped, $? is the pipe's status, and | head -N turns even ${PIPESTATUS[0]}/pipefail green: head closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the exit code itself.

Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:

node scripts/check-test-completeness.mjs -> 3 (no pipe)
node ... 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
node ... 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3

| head did not turn ${PIPESTATUS[0]} or pipefail green. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes, pnpm exec in front, a bash wrapper): every one preserved its non-zero code through | head -1.

And EPIPE genuinely happens — this is not "head read everything":

node -e 'process.stdout.on("error", e => process.stderr.write("PRODUCER-SAW: "+e.code+"\n")); ...exit(7)' | head -1
PRODUCER-SAW: EPIPE
${PIPESTATUS[0]} = 7

The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives | head intact.

What is actually true, and what all nine sites now say:

  • The false green is bare $? after any pipe — it is the LAST command's status, and head/tail both essentially never fail. It is not a property of one shape, and no choice of shape repairs it.
  • ${PIPESTATUS[0]}/pipefail do recover the gate's own code. | tail reads to EOF and forwards it.
  • | head -N closes the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141: seq 1 100000000 | head -1 reports ${PIPESTATUS[0]} = 141. That is a false red, the opposite direction from the one claimed.

Because the reference site would otherwise diverge from the other eight and keep the false claim, scripts/check-i18n-bundles.mjs is corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.

The measurement is anchored where the next author will look: a block comment on reportPrerequisiteNotMet, with an explicit "do NOT write that | head turns PIPESTATUS/pipefail green" and why.

Census instrument

Verbatim, the command used to find the sites:

git grep -n "reads green either way"
git grep -n "the PIPE's status" -- 'scripts/**' 'packages/**'

The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:

git grep -n "no pipe shape repairs it"

which now returns all nine sites across the six files.

Inheritor count: measured 45, not 44 and not 43

Derivation, verbatim:

git grep -lE "^import .*from '.*import-prerequisite\.mjs'"

45 files. Both circulating numbers are wrong, in opposite directions:

  • Triage's 44 is git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one — scripts/pm/dispatch-gates.mjs — only mentions the filename in a comment and imports nothing. So under scripts/** the real importer count is 42.
  • The dispatch order's corrected 43 fixes the self-reference but keeps the scripts/** pathspec, which is itself too narrow: three real importers live in packages/lint/scripts/ and no scripts/** glob can see them.

42 + 3 = 45. Verified live: the corrected banner rendered from packages/lint/scripts/check-doc-formula-expressions.mjs during this PR's gate run.

A2.4 inherit-vs-shadow split: the sets are disjoint

# importers.txt : the 45 paths from the derivation above, sorted
# shadows.txt : git grep -ln "PREREQUISITE NOT MET" , sorted
comm -12 importers.txt shadows.txt -> empty

Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (check-i18n-coverage.mjs, check-half-states.mjs, ci-failure.mjs, check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.

(scripts/check-dual-build-cjs-loads.mjs also carries a local PREREQUISITE NOT MET banner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)

Sites the card's instrument missed

Two, both surfaced by the mechanism census:

  1. scripts/pm/ci-failure.mjs:170 — a module-header comment, Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.
  2. packages/cli/scripts/check-app-nav-i18n.mjs:620 — true but incomplete (prescribes echo "EXIT=$?" without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding] packages/cli/scripts/check-app-nav-i18n.mjs prerequisite banner says echo "EXIT=$?" but never says BEFORE any pipe, and names no shape #13825.

Judged out of class and deliberately untouched: three .github/workflows/*.yml comments that say cmd | tail reports the pipe's status. Each is scoped to bare $?, where it is true, and each sits beside code that already does redirect-then-capture.

check:ratchet-remedy-authority (A2.5)

The dispatch was right that no path derivation names it. node scripts/pm/dispatch-gates.mjs derived 23 families for this diff and check:ratchet-remedy-authority is not among them. Run explicitly anyway — its own verdict line:

OK check-ratchet-remedy-authority: 176 scripts swept (scripts/*.{mjs,mts}); 11 mark the
expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it down outright, 160 hand out no
ratchet-expanding remedy.

Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.

Tests

All gate results below are from 8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.

Self-tests — the constraint the card names (each must stay green, no exit code may change):

gate--self-test
scripts/import-prerequisite.mjs0 — 41 cases (was 36; 5 added)
scripts/check-i18n-coverage.mjs0
scripts/check-i18n-bundles.mjs0
scripts/check-test-completeness.mjs0
scripts/pm/check-half-states.mjs0 — 1826 cases
scripts/pm/ci-failure.mjs0

Exit codes unchanged, spot-checked on the live refusal path: node scripts/check-test-completeness.mjs answered 3 before the edit and 3 after.

New pins.reportPrerequisiteNotMet is split into a pure text builder plus a thin printer (no new export, no caller change, process.exit(1) untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green; | tail kept as the forwarding shape; | head -N plus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats ${PIPESTATUS[0]}/pipefail.

Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a git hash-object move (55b7512b -> b511968f); no build/dist leg exists here, these .mjs files are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to 55b7512b, git diff HEAD empty, git status --porcelain empty, self-test back to 41/41 green.

Derived gate union — 25 commands, all green.node scripts/pm/dispatch-gates.mjs (no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, plus check:ratchet-remedy-authority and check:nul-bytes.

Two of them are not locally runnable in their bare form and were run in the shape CI uses: check:partof-closing-keyword needs PR_BODY (its package.json script is --self-test only, which is what CI runs), and node scripts/check-test-completeness.mjs needs a real CI log (CI runs it inside Test Core). Three gates first exited 1 with a PREREQUISITE NOT MET refusal because this worktree had no node_modules yet — read as NOT MEASURED, not red; all three went green after pnpm install. check:i18n and check:i18n-coverage each refused first for an unbuilt closure and both went green once the closure they named was built.

ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide pnpm lint:

pnpm exec eslint --no-inline-config --format json scripts/
-> exit 0 · 204 files · 0 errors · 0 warnings
  1. The population is eslint's own — it resolved 204 files under scripts/ from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.
  2. The count is read from --format json, not from console text.
  3. Config invariance: this repo runs one eslint.config.mjs which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for any file — stated at eslint.config.mjs:327-329 and measured there with a positive control. So no verdict on an untouched file can move because of this diff.

Control bytes:check:nul-bytes exit 0 (7594 files scanned), plus a direct grep -naP control-character scan over the six changed files — clean.

Changeset: skip-changeset, and why this contradicts the dispatch order

The dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:

  • Nothing here ships. The root package is private: true with no files, and @objectstack/lint — the only package with importers in this diff's blast radius — publishes ["dist","README.md","CHANGELOG.md"], so packages/lint/scripts/ is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.
  • An empty-frontmatter changeset is mechanically rejected: scripts/check-empty-changeset.mjs refuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input to changesets/action and can stall a release; the label produces no input at all).

So skip-changeset is the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are root scripts/*.mjs rather than scripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.


Generated by Claude Code

…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 4cc99bcAug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13429-pipe-shape-advisory-siblings branch August 31, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner by claude[bot] · Pull Request #13828 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings
Aug 31, 2026
Merged

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner#13828
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13429

Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.

⚠️ Read this first: the prescribed mechanism is factually wrong, and this PR does not propagate it

Zone 1 binds me to name | head -N and the EPIPE mechanism, to name the safe capture, and to keep | tail identified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:

Piped, $? is the pipe's status, and | head -N turns even ${PIPESTATUS[0]}/pipefail green: head closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the exit code itself.

Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:

node scripts/check-test-completeness.mjs -> 3 (no pipe)
node ... 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
node ... 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3

| head did not turn ${PIPESTATUS[0]} or pipefail green. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes, pnpm exec in front, a bash wrapper): every one preserved its non-zero code through | head -1.

And EPIPE genuinely happens — this is not "head read everything":

node -e 'process.stdout.on("error", e => process.stderr.write("PRODUCER-SAW: "+e.code+"\n")); ...exit(7)' | head -1
PRODUCER-SAW: EPIPE
${PIPESTATUS[0]} = 7

The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives | head intact.

What is actually true, and what all nine sites now say:

  • The false green is bare $? after any pipe — it is the LAST command's status, and head/tail both essentially never fail. It is not a property of one shape, and no choice of shape repairs it.
  • ${PIPESTATUS[0]}/pipefail do recover the gate's own code. | tail reads to EOF and forwards it.
  • | head -N closes the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141: seq 1 100000000 | head -1 reports ${PIPESTATUS[0]} = 141. That is a false red, the opposite direction from the one claimed.

Because the reference site would otherwise diverge from the other eight and keep the false claim, scripts/check-i18n-bundles.mjs is corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.

The measurement is anchored where the next author will look: a block comment on reportPrerequisiteNotMet, with an explicit "do NOT write that | head turns PIPESTATUS/pipefail green" and why.

Census instrument

Verbatim, the command used to find the sites:

git grep -n "reads green either way"
git grep -n "the PIPE's status" -- 'scripts/**' 'packages/**'

The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:

git grep -n "no pipe shape repairs it"

which now returns all nine sites across the six files.

Inheritor count: measured 45, not 44 and not 43

Derivation, verbatim:

git grep -lE "^import .*from '.*import-prerequisite\.mjs'"

45 files. Both circulating numbers are wrong, in opposite directions:

  • Triage's 44 is git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one — scripts/pm/dispatch-gates.mjs — only mentions the filename in a comment and imports nothing. So under scripts/** the real importer count is 42.
  • The dispatch order's corrected 43 fixes the self-reference but keeps the scripts/** pathspec, which is itself too narrow: three real importers live in packages/lint/scripts/ and no scripts/** glob can see them.

42 + 3 = 45. Verified live: the corrected banner rendered from packages/lint/scripts/check-doc-formula-expressions.mjs during this PR's gate run.

A2.4 inherit-vs-shadow split: the sets are disjoint

# importers.txt : the 45 paths from the derivation above, sorted
# shadows.txt : git grep -ln "PREREQUISITE NOT MET" , sorted
comm -12 importers.txt shadows.txt -> empty

Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (check-i18n-coverage.mjs, check-half-states.mjs, ci-failure.mjs, check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.

(scripts/check-dual-build-cjs-loads.mjs also carries a local PREREQUISITE NOT MET banner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)

Sites the card's instrument missed

Two, both surfaced by the mechanism census:

  1. scripts/pm/ci-failure.mjs:170 — a module-header comment, Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.
  2. packages/cli/scripts/check-app-nav-i18n.mjs:620 — true but incomplete (prescribes echo "EXIT=$?" without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding] packages/cli/scripts/check-app-nav-i18n.mjs prerequisite banner says echo "EXIT=$?" but never says BEFORE any pipe, and names no shape #13825.

Judged out of class and deliberately untouched: three .github/workflows/*.yml comments that say cmd | tail reports the pipe's status. Each is scoped to bare $?, where it is true, and each sits beside code that already does redirect-then-capture.

check:ratchet-remedy-authority (A2.5)

The dispatch was right that no path derivation names it. node scripts/pm/dispatch-gates.mjs derived 23 families for this diff and check:ratchet-remedy-authority is not among them. Run explicitly anyway — its own verdict line:

OK check-ratchet-remedy-authority: 176 scripts swept (scripts/*.{mjs,mts}); 11 mark the
expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it down outright, 160 hand out no
ratchet-expanding remedy.

Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.

Tests

All gate results below are from 8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.

Self-tests — the constraint the card names (each must stay green, no exit code may change):

gate--self-test
scripts/import-prerequisite.mjs0 — 41 cases (was 36; 5 added)
scripts/check-i18n-coverage.mjs0
scripts/check-i18n-bundles.mjs0
scripts/check-test-completeness.mjs0
scripts/pm/check-half-states.mjs0 — 1826 cases
scripts/pm/ci-failure.mjs0

Exit codes unchanged, spot-checked on the live refusal path: node scripts/check-test-completeness.mjs answered 3 before the edit and 3 after.

New pins.reportPrerequisiteNotMet is split into a pure text builder plus a thin printer (no new export, no caller change, process.exit(1) untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green; | tail kept as the forwarding shape; | head -N plus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats ${PIPESTATUS[0]}/pipefail.

Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a git hash-object move (55b7512b -> b511968f); no build/dist leg exists here, these .mjs files are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to 55b7512b, git diff HEAD empty, git status --porcelain empty, self-test back to 41/41 green.

Derived gate union — 25 commands, all green.node scripts/pm/dispatch-gates.mjs (no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, plus check:ratchet-remedy-authority and check:nul-bytes.

Two of them are not locally runnable in their bare form and were run in the shape CI uses: check:partof-closing-keyword needs PR_BODY (its package.json script is --self-test only, which is what CI runs), and node scripts/check-test-completeness.mjs needs a real CI log (CI runs it inside Test Core). Three gates first exited 1 with a PREREQUISITE NOT MET refusal because this worktree had no node_modules yet — read as NOT MEASURED, not red; all three went green after pnpm install. check:i18n and check:i18n-coverage each refused first for an unbuilt closure and both went green once the closure they named was built.

ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide pnpm lint:

pnpm exec eslint --no-inline-config --format json scripts/
-> exit 0 · 204 files · 0 errors · 0 warnings
  1. The population is eslint's own — it resolved 204 files under scripts/ from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.
  2. The count is read from --format json, not from console text.
  3. Config invariance: this repo runs one eslint.config.mjs which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for any file — stated at eslint.config.mjs:327-329 and measured there with a positive control. So no verdict on an untouched file can move because of this diff.

Control bytes:check:nul-bytes exit 0 (7594 files scanned), plus a direct grep -naP control-character scan over the six changed files — clean.

Changeset: skip-changeset, and why this contradicts the dispatch order

The dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:

  • Nothing here ships. The root package is private: true with no files, and @objectstack/lint — the only package with importers in this diff's blast radius — publishes ["dist","README.md","CHANGELOG.md"], so packages/lint/scripts/ is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.
  • An empty-frontmatter changeset is mechanically rejected: scripts/check-empty-changeset.mjs refuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input to changesets/action and can stall a release; the label produces no input at all).

So skip-changeset is the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are root scripts/*.mjs rather than scripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.


Generated by Claude Code

…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 4cc99bcAug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13429-pipe-shape-advisory-siblings branch August 31, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner by claude[bot] · Pull Request #13828 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings
Aug 31, 2026
Merged

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner#13828
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13429

Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.

⚠️ Read this first: the prescribed mechanism is factually wrong, and this PR does not propagate it

Zone 1 binds me to name | head -N and the EPIPE mechanism, to name the safe capture, and to keep | tail identified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:

Piped, $? is the pipe's status, and | head -N turns even ${PIPESTATUS[0]}/pipefail green: head closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the exit code itself.

Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:

node scripts/check-test-completeness.mjs -> 3 (no pipe)
node ... 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
node ... 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3

| head did not turn ${PIPESTATUS[0]} or pipefail green. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes, pnpm exec in front, a bash wrapper): every one preserved its non-zero code through | head -1.

And EPIPE genuinely happens — this is not "head read everything":

node -e 'process.stdout.on("error", e => process.stderr.write("PRODUCER-SAW: "+e.code+"\n")); ...exit(7)' | head -1
PRODUCER-SAW: EPIPE
${PIPESTATUS[0]} = 7

The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives | head intact.

What is actually true, and what all nine sites now say:

  • The false green is bare $? after any pipe — it is the LAST command's status, and head/tail both essentially never fail. It is not a property of one shape, and no choice of shape repairs it.
  • ${PIPESTATUS[0]}/pipefail do recover the gate's own code. | tail reads to EOF and forwards it.
  • | head -N closes the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141: seq 1 100000000 | head -1 reports ${PIPESTATUS[0]} = 141. That is a false red, the opposite direction from the one claimed.

Because the reference site would otherwise diverge from the other eight and keep the false claim, scripts/check-i18n-bundles.mjs is corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.

The measurement is anchored where the next author will look: a block comment on reportPrerequisiteNotMet, with an explicit "do NOT write that | head turns PIPESTATUS/pipefail green" and why.

Census instrument

Verbatim, the command used to find the sites:

git grep -n "reads green either way"
git grep -n "the PIPE's status" -- 'scripts/**' 'packages/**'

The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:

git grep -n "no pipe shape repairs it"

which now returns all nine sites across the six files.

Inheritor count: measured 45, not 44 and not 43

Derivation, verbatim:

git grep -lE "^import .*from '.*import-prerequisite\.mjs'"

45 files. Both circulating numbers are wrong, in opposite directions:

  • Triage's 44 is git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one — scripts/pm/dispatch-gates.mjs — only mentions the filename in a comment and imports nothing. So under scripts/** the real importer count is 42.
  • The dispatch order's corrected 43 fixes the self-reference but keeps the scripts/** pathspec, which is itself too narrow: three real importers live in packages/lint/scripts/ and no scripts/** glob can see them.

42 + 3 = 45. Verified live: the corrected banner rendered from packages/lint/scripts/check-doc-formula-expressions.mjs during this PR's gate run.

A2.4 inherit-vs-shadow split: the sets are disjoint

# importers.txt : the 45 paths from the derivation above, sorted
# shadows.txt : git grep -ln "PREREQUISITE NOT MET" , sorted
comm -12 importers.txt shadows.txt -> empty

Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (check-i18n-coverage.mjs, check-half-states.mjs, ci-failure.mjs, check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.

(scripts/check-dual-build-cjs-loads.mjs also carries a local PREREQUISITE NOT MET banner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)

Sites the card's instrument missed

Two, both surfaced by the mechanism census:

  1. scripts/pm/ci-failure.mjs:170 — a module-header comment, Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.
  2. packages/cli/scripts/check-app-nav-i18n.mjs:620 — true but incomplete (prescribes echo "EXIT=$?" without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding] packages/cli/scripts/check-app-nav-i18n.mjs prerequisite banner says echo "EXIT=$?" but never says BEFORE any pipe, and names no shape #13825.

Judged out of class and deliberately untouched: three .github/workflows/*.yml comments that say cmd | tail reports the pipe's status. Each is scoped to bare $?, where it is true, and each sits beside code that already does redirect-then-capture.

check:ratchet-remedy-authority (A2.5)

The dispatch was right that no path derivation names it. node scripts/pm/dispatch-gates.mjs derived 23 families for this diff and check:ratchet-remedy-authority is not among them. Run explicitly anyway — its own verdict line:

OK check-ratchet-remedy-authority: 176 scripts swept (scripts/*.{mjs,mts}); 11 mark the
expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it down outright, 160 hand out no
ratchet-expanding remedy.

Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.

Tests

All gate results below are from 8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.

Self-tests — the constraint the card names (each must stay green, no exit code may change):

gate--self-test
scripts/import-prerequisite.mjs0 — 41 cases (was 36; 5 added)
scripts/check-i18n-coverage.mjs0
scripts/check-i18n-bundles.mjs0
scripts/check-test-completeness.mjs0
scripts/pm/check-half-states.mjs0 — 1826 cases
scripts/pm/ci-failure.mjs0

Exit codes unchanged, spot-checked on the live refusal path: node scripts/check-test-completeness.mjs answered 3 before the edit and 3 after.

New pins.reportPrerequisiteNotMet is split into a pure text builder plus a thin printer (no new export, no caller change, process.exit(1) untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green; | tail kept as the forwarding shape; | head -N plus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats ${PIPESTATUS[0]}/pipefail.

Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a git hash-object move (55b7512b -> b511968f); no build/dist leg exists here, these .mjs files are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to 55b7512b, git diff HEAD empty, git status --porcelain empty, self-test back to 41/41 green.

Derived gate union — 25 commands, all green.node scripts/pm/dispatch-gates.mjs (no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, plus check:ratchet-remedy-authority and check:nul-bytes.

Two of them are not locally runnable in their bare form and were run in the shape CI uses: check:partof-closing-keyword needs PR_BODY (its package.json script is --self-test only, which is what CI runs), and node scripts/check-test-completeness.mjs needs a real CI log (CI runs it inside Test Core). Three gates first exited 1 with a PREREQUISITE NOT MET refusal because this worktree had no node_modules yet — read as NOT MEASURED, not red; all three went green after pnpm install. check:i18n and check:i18n-coverage each refused first for an unbuilt closure and both went green once the closure they named was built.

ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide pnpm lint:

pnpm exec eslint --no-inline-config --format json scripts/
-> exit 0 · 204 files · 0 errors · 0 warnings
  1. The population is eslint's own — it resolved 204 files under scripts/ from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.
  2. The count is read from --format json, not from console text.
  3. Config invariance: this repo runs one eslint.config.mjs which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for any file — stated at eslint.config.mjs:327-329 and measured there with a positive control. So no verdict on an untouched file can move because of this diff.

Control bytes:check:nul-bytes exit 0 (7594 files scanned), plus a direct grep -naP control-character scan over the six changed files — clean.

Changeset: skip-changeset, and why this contradicts the dispatch order

The dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:

  • Nothing here ships. The root package is private: true with no files, and @objectstack/lint — the only package with importers in this diff's blast radius — publishes ["dist","README.md","CHANGELOG.md"], so packages/lint/scripts/ is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.
  • An empty-frontmatter changeset is mechanically rejected: scripts/check-empty-changeset.mjs refuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input to changesets/action and can stall a release; the label produces no input at all).

So skip-changeset is the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are root scripts/*.mjs rather than scripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.


Generated by Claude Code

…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 4cc99bcAug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13429-pipe-shape-advisory-siblings branch August 31, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner by claude[bot] · Pull Request #13828 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings
Aug 31, 2026
Merged

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner#13828
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13429

Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.

⚠️ Read this first: the prescribed mechanism is factually wrong, and this PR does not propagate it

Zone 1 binds me to name | head -N and the EPIPE mechanism, to name the safe capture, and to keep | tail identified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:

Piped, $? is the pipe's status, and | head -N turns even ${PIPESTATUS[0]}/pipefail green: head closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the exit code itself.

Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:

node scripts/check-test-completeness.mjs -> 3 (no pipe)
node ... 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
node ... 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3

| head did not turn ${PIPESTATUS[0]} or pipefail green. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes, pnpm exec in front, a bash wrapper): every one preserved its non-zero code through | head -1.

And EPIPE genuinely happens — this is not "head read everything":

node -e 'process.stdout.on("error", e => process.stderr.write("PRODUCER-SAW: "+e.code+"\n")); ...exit(7)' | head -1
PRODUCER-SAW: EPIPE
${PIPESTATUS[0]} = 7

The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives | head intact.

What is actually true, and what all nine sites now say:

  • The false green is bare $? after any pipe — it is the LAST command's status, and head/tail both essentially never fail. It is not a property of one shape, and no choice of shape repairs it.
  • ${PIPESTATUS[0]}/pipefail do recover the gate's own code. | tail reads to EOF and forwards it.
  • | head -N closes the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141: seq 1 100000000 | head -1 reports ${PIPESTATUS[0]} = 141. That is a false red, the opposite direction from the one claimed.

Because the reference site would otherwise diverge from the other eight and keep the false claim, scripts/check-i18n-bundles.mjs is corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.

The measurement is anchored where the next author will look: a block comment on reportPrerequisiteNotMet, with an explicit "do NOT write that | head turns PIPESTATUS/pipefail green" and why.

Census instrument

Verbatim, the command used to find the sites:

git grep -n "reads green either way"
git grep -n "the PIPE's status" -- 'scripts/**' 'packages/**'

The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:

git grep -n "no pipe shape repairs it"

which now returns all nine sites across the six files.

Inheritor count: measured 45, not 44 and not 43

Derivation, verbatim:

git grep -lE "^import .*from '.*import-prerequisite\.mjs'"

45 files. Both circulating numbers are wrong, in opposite directions:

  • Triage's 44 is git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one — scripts/pm/dispatch-gates.mjs — only mentions the filename in a comment and imports nothing. So under scripts/** the real importer count is 42.
  • The dispatch order's corrected 43 fixes the self-reference but keeps the scripts/** pathspec, which is itself too narrow: three real importers live in packages/lint/scripts/ and no scripts/** glob can see them.

42 + 3 = 45. Verified live: the corrected banner rendered from packages/lint/scripts/check-doc-formula-expressions.mjs during this PR's gate run.

A2.4 inherit-vs-shadow split: the sets are disjoint

# importers.txt : the 45 paths from the derivation above, sorted
# shadows.txt : git grep -ln "PREREQUISITE NOT MET" , sorted
comm -12 importers.txt shadows.txt -> empty

Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (check-i18n-coverage.mjs, check-half-states.mjs, ci-failure.mjs, check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.

(scripts/check-dual-build-cjs-loads.mjs also carries a local PREREQUISITE NOT MET banner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)

Sites the card's instrument missed

Two, both surfaced by the mechanism census:

  1. scripts/pm/ci-failure.mjs:170 — a module-header comment, Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.
  2. packages/cli/scripts/check-app-nav-i18n.mjs:620 — true but incomplete (prescribes echo "EXIT=$?" without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding] packages/cli/scripts/check-app-nav-i18n.mjs prerequisite banner says echo "EXIT=$?" but never says BEFORE any pipe, and names no shape #13825.

Judged out of class and deliberately untouched: three .github/workflows/*.yml comments that say cmd | tail reports the pipe's status. Each is scoped to bare $?, where it is true, and each sits beside code that already does redirect-then-capture.

check:ratchet-remedy-authority (A2.5)

The dispatch was right that no path derivation names it. node scripts/pm/dispatch-gates.mjs derived 23 families for this diff and check:ratchet-remedy-authority is not among them. Run explicitly anyway — its own verdict line:

OK check-ratchet-remedy-authority: 176 scripts swept (scripts/*.{mjs,mts}); 11 mark the
expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it down outright, 160 hand out no
ratchet-expanding remedy.

Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.

Tests

All gate results below are from 8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.

Self-tests — the constraint the card names (each must stay green, no exit code may change):

gate--self-test
scripts/import-prerequisite.mjs0 — 41 cases (was 36; 5 added)
scripts/check-i18n-coverage.mjs0
scripts/check-i18n-bundles.mjs0
scripts/check-test-completeness.mjs0
scripts/pm/check-half-states.mjs0 — 1826 cases
scripts/pm/ci-failure.mjs0

Exit codes unchanged, spot-checked on the live refusal path: node scripts/check-test-completeness.mjs answered 3 before the edit and 3 after.

New pins.reportPrerequisiteNotMet is split into a pure text builder plus a thin printer (no new export, no caller change, process.exit(1) untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green; | tail kept as the forwarding shape; | head -N plus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats ${PIPESTATUS[0]}/pipefail.

Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a git hash-object move (55b7512b -> b511968f); no build/dist leg exists here, these .mjs files are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to 55b7512b, git diff HEAD empty, git status --porcelain empty, self-test back to 41/41 green.

Derived gate union — 25 commands, all green.node scripts/pm/dispatch-gates.mjs (no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, plus check:ratchet-remedy-authority and check:nul-bytes.

Two of them are not locally runnable in their bare form and were run in the shape CI uses: check:partof-closing-keyword needs PR_BODY (its package.json script is --self-test only, which is what CI runs), and node scripts/check-test-completeness.mjs needs a real CI log (CI runs it inside Test Core). Three gates first exited 1 with a PREREQUISITE NOT MET refusal because this worktree had no node_modules yet — read as NOT MEASURED, not red; all three went green after pnpm install. check:i18n and check:i18n-coverage each refused first for an unbuilt closure and both went green once the closure they named was built.

ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide pnpm lint:

pnpm exec eslint --no-inline-config --format json scripts/
-> exit 0 · 204 files · 0 errors · 0 warnings
  1. The population is eslint's own — it resolved 204 files under scripts/ from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.
  2. The count is read from --format json, not from console text.
  3. Config invariance: this repo runs one eslint.config.mjs which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for any file — stated at eslint.config.mjs:327-329 and measured there with a positive control. So no verdict on an untouched file can move because of this diff.

Control bytes:check:nul-bytes exit 0 (7594 files scanned), plus a direct grep -naP control-character scan over the six changed files — clean.

Changeset: skip-changeset, and why this contradicts the dispatch order

The dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:

  • Nothing here ships. The root package is private: true with no files, and @objectstack/lint — the only package with importers in this diff's blast radius — publishes ["dist","README.md","CHANGELOG.md"], so packages/lint/scripts/ is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.
  • An empty-frontmatter changeset is mechanically rejected: scripts/check-empty-changeset.mjs refuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input to changesets/action and can stall a release; the label produces no input at all).

So skip-changeset is the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are root scripts/*.mjs rather than scripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.


Generated by Claude Code

…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 4cc99bcAug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13429-pipe-shape-advisory-siblings branch August 31, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner by claude[bot] · Pull Request #13828 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings
Aug 31, 2026
Merged

fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner#13828
os-project-manager merged 2 commits into
mainfrom
claude/issue-13429-pipe-shape-advisory-siblings

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13429

Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.

⚠️ Read this first: the prescribed mechanism is factually wrong, and this PR does not propagate it

Zone 1 binds me to name | head -N and the EPIPE mechanism, to name the safe capture, and to keep | tail identified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:

Piped, $? is the pipe's status, and | head -N turns even ${PIPESTATUS[0]}/pipefail green: head closes the read end early, so this gate takes EPIPE and exits 0 — the pipe changed the exit code itself.

Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:

node scripts/check-test-completeness.mjs -> 3 (no pipe)
node ... 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
node ... 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3

| head did not turn ${PIPESTATUS[0]} or pipefail green. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes, pnpm exec in front, a bash wrapper): every one preserved its non-zero code through | head -1.

And EPIPE genuinely happens — this is not "head read everything":

node -e 'process.stdout.on("error", e => process.stderr.write("PRODUCER-SAW: "+e.code+"\n")); ...exit(7)' | head -1
PRODUCER-SAW: EPIPE
${PIPESTATUS[0]} = 7

The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives | head intact.

What is actually true, and what all nine sites now say:

  • The false green is bare $? after any pipe — it is the LAST command's status, and head/tail both essentially never fail. It is not a property of one shape, and no choice of shape repairs it.
  • ${PIPESTATUS[0]}/pipefail do recover the gate's own code. | tail reads to EOF and forwards it.
  • | head -N closes the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141: seq 1 100000000 | head -1 reports ${PIPESTATUS[0]} = 141. That is a false red, the opposite direction from the one claimed.

Because the reference site would otherwise diverge from the other eight and keep the false claim, scripts/check-i18n-bundles.mjs is corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.

The measurement is anchored where the next author will look: a block comment on reportPrerequisiteNotMet, with an explicit "do NOT write that | head turns PIPESTATUS/pipefail green" and why.

Census instrument

Verbatim, the command used to find the sites:

git grep -n "reads green either way"
git grep -n "the PIPE's status" -- 'scripts/**' 'packages/**'

The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:

git grep -n "no pipe shape repairs it"

which now returns all nine sites across the six files.

Inheritor count: measured 45, not 44 and not 43

Derivation, verbatim:

git grep -lE "^import .*from '.*import-prerequisite\.mjs'"

45 files. Both circulating numbers are wrong, in opposite directions:

  • Triage's 44 is git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one — scripts/pm/dispatch-gates.mjs — only mentions the filename in a comment and imports nothing. So under scripts/** the real importer count is 42.
  • The dispatch order's corrected 43 fixes the self-reference but keeps the scripts/** pathspec, which is itself too narrow: three real importers live in packages/lint/scripts/ and no scripts/** glob can see them.

42 + 3 = 45. Verified live: the corrected banner rendered from packages/lint/scripts/check-doc-formula-expressions.mjs during this PR's gate run.

A2.4 inherit-vs-shadow split: the sets are disjoint

# importers.txt : the 45 paths from the derivation above, sorted
# shadows.txt : git grep -ln "PREREQUISITE NOT MET" , sorted
comm -12 importers.txt shadows.txt -> empty

Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (check-i18n-coverage.mjs, check-half-states.mjs, ci-failure.mjs, check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.

(scripts/check-dual-build-cjs-loads.mjs also carries a local PREREQUISITE NOT MET banner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)

Sites the card's instrument missed

Two, both surfaced by the mechanism census:

  1. scripts/pm/ci-failure.mjs:170 — a module-header comment, Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.
  2. packages/cli/scripts/check-app-nav-i18n.mjs:620 — true but incomplete (prescribes echo "EXIT=$?" without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding] packages/cli/scripts/check-app-nav-i18n.mjs prerequisite banner says echo "EXIT=$?" but never says BEFORE any pipe, and names no shape #13825.

Judged out of class and deliberately untouched: three .github/workflows/*.yml comments that say cmd | tail reports the pipe's status. Each is scoped to bare $?, where it is true, and each sits beside code that already does redirect-then-capture.

check:ratchet-remedy-authority (A2.5)

The dispatch was right that no path derivation names it. node scripts/pm/dispatch-gates.mjs derived 23 families for this diff and check:ratchet-remedy-authority is not among them. Run explicitly anyway — its own verdict line:

OK check-ratchet-remedy-authority: 176 scripts swept (scripts/*.{mjs,mts}); 11 mark the
expanding remedy ⛔ MAINTAINER-ONLY, 5 turn it down outright, 160 hand out no
ratchet-expanding remedy.

Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.

Tests

All gate results below are from 8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.

Self-tests — the constraint the card names (each must stay green, no exit code may change):

gate--self-test
scripts/import-prerequisite.mjs0 — 41 cases (was 36; 5 added)
scripts/check-i18n-coverage.mjs0
scripts/check-i18n-bundles.mjs0
scripts/check-test-completeness.mjs0
scripts/pm/check-half-states.mjs0 — 1826 cases
scripts/pm/ci-failure.mjs0

Exit codes unchanged, spot-checked on the live refusal path: node scripts/check-test-completeness.mjs answered 3 before the edit and 3 after.

New pins.reportPrerequisiteNotMet is split into a pure text builder plus a thin printer (no new export, no caller change, process.exit(1) untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green; | tail kept as the forwarding shape; | head -N plus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats ${PIPESTATUS[0]}/pipefail.

Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a git hash-object move (55b7512b -> b511968f); no build/dist leg exists here, these .mjs files are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to 55b7512b, git diff HEAD empty, git status --porcelain empty, self-test back to 41/41 green.

Derived gate union — 25 commands, all green.node scripts/pm/dispatch-gates.mjs (no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, plus check:ratchet-remedy-authority and check:nul-bytes.

Two of them are not locally runnable in their bare form and were run in the shape CI uses: check:partof-closing-keyword needs PR_BODY (its package.json script is --self-test only, which is what CI runs), and node scripts/check-test-completeness.mjs needs a real CI log (CI runs it inside Test Core). Three gates first exited 1 with a PREREQUISITE NOT MET refusal because this worktree had no node_modules yet — read as NOT MEASURED, not red; all three went green after pnpm install. check:i18n and check:i18n-coverage each refused first for an unbuilt closure and both went green once the closure they named was built.

ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide pnpm lint:

pnpm exec eslint --no-inline-config --format json scripts/
-> exit 0 · 204 files · 0 errors · 0 warnings
  1. The population is eslint's own — it resolved 204 files under scripts/ from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.
  2. The count is read from --format json, not from console text.
  3. Config invariance: this repo runs one eslint.config.mjs which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for any file — stated at eslint.config.mjs:327-329 and measured there with a positive control. So no verdict on an untouched file can move because of this diff.

Control bytes:check:nul-bytes exit 0 (7594 files scanned), plus a direct grep -naP control-character scan over the six changed files — clean.

Changeset: skip-changeset, and why this contradicts the dispatch order

The dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:

  • Nothing here ships. The root package is private: true with no files, and @objectstack/lint — the only package with importers in this diff's blast radius — publishes ["dist","README.md","CHANGELOG.md"], so packages/lint/scripts/ is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.
  • An empty-frontmatter changeset is mechanically rejected: scripts/check-empty-changeset.mjs refuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input to changesets/action and can stall a release; the label produces no input at all).

So skip-changeset is the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are root scripts/*.mjs rather than scripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.


Generated by Claude Code

…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test: FAIL src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
    ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 4cc99bcAug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13429-pipe-shape-advisory-siblings branch August 31, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude