fix: answer name-membership in-process, not through a pipe into grep -Fxq - #19
Merged
Merged
Conversation
…-Fxq `--verify` intermittently condemned a healthy link as stale on macOS, failing the smoke job on master (run 29, 20d91bf) and again on #18's first commit: install.sh: line 104: printf: write error: Broken pipe [ERR] stale damascus-owned link: .../.claude/skills/bdd-tdd-execution [ERR] 1 problem(s) found — re-run install.sh to repair FAIL: --verify failed after repair Four call sites asked "is this name one we ship?" as `<emit names> | grep -Fxq`. grep matches correctly and exits on the first hit — but every name emitted after that hit then writes into a closed pipe, and `set -o pipefail` (line 16) promotes that EPIPE to the pipeline's exit status. So a name damascus DOES ship reads as one it does not, and the link is pruned or flagged. Only a match that is not the last line emitted can trigger it, and only when the producer is slower than grep — hence macOS-only and intermittent. The victim was `bdd-tdd-execution`, an alias that is not last in ALIASES. Reproduced deterministically by making the producer slower than the consumer: a match on the first or a middle name reads as MISSED through the pipe and MATCH through a loop; a match on the last name reads correctly through both, because nothing is written after it. `expected_skill_names` (emitter) becomes `is_expected_skill` (predicate), plus `is_expected_agent` for the two AGENTS sites. Plain loops with an early return: no subshell, no pipe, no reader that can exit first. bash 3.2 safe, and the `if ...; then return 0; fi` form avoids the `&& return` errexit footgun. Verified: all 25 shipped names (6 stages, 8 KEEP, 6 aliases, 5 agents) accepted, including bdd-tdd-execution; unknown names still rejected; smoke suite 10/10 consecutive runs with zero "Broken pipe" in the output; sourcing guard intact so CI can still read the manifests. CHANGELOG records the guarantee on the existing install.sh Added line rather than as a Fixed entry: [Unreleased] states there is no earlier release to fix against, and this bug never shipped in one. Feature: install-link-verification Symbol: is_expected_skill,is_expected_agent,expected_skill_names,pipefail,SIGPIPE Errors: write error: Broken pipe,stale damascus-owned link,--verify failed after repair Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQQrX3uYoeNyqxYkSbRp2W
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The symptom
--verifyintermittently condemned a healthy link as stale on macOS, failing the smoke job on master (run 29,20d91bf, 2026-09-09) and again on #18's first commit:Not a flake. It reproduces on demand once the mechanism is understood.
The mechanism
Four call sites asked "is this name one we ship?" as a pipeline:
grep -Fxqmatches correctly and exits on the first hit. Every name emitted after that hit then writes into a closed pipe, andset -o pipefail(install.sh:16) promotes the producer's EPIPE to the pipeline's exit status. So a name damascus does ship reads as one it does not — and the link is pruned or flagged stale.Two conditions gate it, which is exactly why it looked random:
The victim,
bdd-tdd-execution, is an alias that is not last inALIASES.Reproduced before fixing
Modelling "producer slower than consumer" makes it deterministic:
A present name reading as
MISSEDthrough the pipe is the bug, in one line.The fix
expected_skill_names(an emitter) becomesis_expected_skill(a predicate), plusis_expected_agentfor the twoAGENTSsites. Plain loops with an earlyreturn: no subshell, no pipe, no reader that can exit first.bash 3.2 safe, and written as
if ...; then return 0; firather than[ ... ] && return 0, which would be anerrexitfootgun underset -e.Verification
bdd-tdd-executionincluded; unknown names (retired-old-name,not-an-agent) still correctly rejectedtests/smoke.sh10/10 consecutive runs, with zeroBroken pipeanywhere in the outputbash -n, alias symlinks, SKILL.md/agent frontmatter, submodule-tag + README table (superpowers @ v6.3.0,spec-kit @ v1.0.6)source install.shto read the manifests; verified by sourcing it and reading all four arraysshellcheckisn't available in this environment, so that specific linter only runs in CI. The change removes two pipelines and adds no new external commands.A note on the CHANGELOG
Recorded as a sharpened guarantee on the existing
install.shAdded line — "prunes damascus-owned links whose names are no longer shipped and never one it still ships" — rather than as aFixedentry.[Unreleased]states there is no earlier release to fix against, and this bug never shipped in one, so aFixedline would describe a regression no reader ever saw. Easy to flip if you'd rather it be explicit.🤖 Generated with Claude Code
https://claude.ai/code/session_01NQQrX3uYoeNyqxYkSbRp2W
Generated by Claude Code