fix(plugin): an early-exiting head no longer voids its own output under pipefail (#4042)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 14s

In a repo where 3,000 files define `slug`, scribe_local_dups printed
nothing; with 3 files it printed the duplicate line. Every hook runs under
`set -uo pipefail`, and `hits=$(git grep -l … | head -4) || hits=""` lost
head's four lines whenever git grep was still writing when head exited. That
is a SIGPIPE, the substitution fails, and the outer fallback wipes the result.
So the by-name duplicate arm went silent for exactly the most-duplicated
names. Found while fixing the same trap in the new Stop hook (#4041).

- The fallback moves inside the substitution, `$(… | head -N || true)`, at all
  five sites: scribe_defs.sh (local dups), scribe_prior_art.sh (names,
  old_first, shapes) and scribe_after_write.sh (names). A real upstream
  failure still yields empty output.
- check_plugin gains a known-bad pattern for the shape, so no hook can bring
  it back.
- Tests: the real function against a 3,000-file repo (past the pipe buffer),
  and the pattern shown to flag the old shape and pass the fix. Plugin
  version minted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-14 18:54:49 -04:00
co-authored by Claude Opus 5
parent c7531d3700
commit 0fab08276c
6 changed files with 85 additions and 6 deletions
+13
View File
@@ -208,6 +208,19 @@ PATTERNS: list[tuple[re.Pattern, str, str]] = [
"produces separate encoded lines joined by raw newlines — an invalid "
"URL. Use -s (slurp) as well, e.g. `jq -sRr '@uri'`.",
),
(
# `$(producer | head -N) || var=""` — the fallback OUTSIDE the
# substitution (#4042). Every hook runs under `pipefail`; once `head`
# (or `grep -m`/`-q`) has what it needs it exits, a producer still
# writing dies of SIGPIPE, the substitution fails, and the fallback
# wipes the output that was already captured. Silent, and only on big
# inputs — which for the duplicate arm meant the most-duplicated names.
re.compile(r"\|\s*(?:head\b|grep\s+-[A-Za-z]*[mq])[^)]*\)\s*\|\|\s*\w+=\"\""),
"early-exit consumer with the fallback outside the substitution",
"Under pipefail, `$(x | head -N) || v=\"\"` discards head's output whenever x "
"is still writing when head exits (SIGPIPE). Put the fallback inside: "
"`$(x | head -N || true)`.",
),
(
re.compile(r"\|\s*cut\s+-c"),
"line-oriented cut for a payload cap",