Baseline rule: a gate's output goes to a file and its exit code comes from the command, never a pipeline #49
Labels
No labels
component:adapters
component:lifecycle
component:qa
component:setup
component:shared
component:worktrees
phase/accepted
phase/backlog
phase/deployed
phase/developing
phase/integrating
phase/planning
phase/qa
phase/requirements
phase/uat
priority:critical
priority:critical
priority:high
priority:high
priority:low
priority:low
priority:medium
priority:medium
type:bug
type:chore
type:docs
type:feature
type:infra
type:tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jbr870/devwork-skills#49
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Rank 13 of 13 in #37 · est. saving ~6m directly; larger as a false-signal class
Evidence
Three occurrences in this feature, all the same shape — a gate's real result hidden by the shell
plumbing around it:
pnpm test 2>&1 | tail -25truncated past thesummary, so the entire 5m 40s suite was run a second time with a different filter.
greppipeline made the harness recordexit code 0while the suite hadactually failed; the orchestrator noted "the harness recorded 'exit code 0' because my grep
succeeded". This fed the 18-minute false alarm in #43.
head -40pipe sent SIGPIPE to a server the model had just started, killing itmid-request and producing an error boundary it briefly read as a product defect.
There is precedent for the fix shape: commit
e3a5369(#33) already requires rc-checked stdoutfor every helper invocation in
phase-outcome. This generalises the same rule to gate commands.Proposed change
A rule in the portability/verification baseline: a gate command writes its full output to a file
and its exit code is read from the command, never from a pipeline. Filter the file afterwards.
Concretely —
cmd > "$OUT" 2>&1; rc=$?then grep$OUT, nevercmd | tail.Two corollaries worth stating: a pipeline's exit status is the last stage's, so
| grepand| tailsilently rewrite a red gate green; and a truncating reader can SIGPIPE a long-runningproducer it is reading from.
Acceptance
scripts/lint-conventions.shflagspnpm test|lint|typecheck|buildpiped directly into a filterin skill-emitted fences.
tail/grepis corrected.nonebranch unreachable #55/meta-amend outcome — amended
Commit
6174683onmain.Verified at HEAD (
2e0e43e)phase-outcome.md, from #33/e3a5369), which governs helper invocations only — the issue's own read of the precedent is correct, and it had not been generalised to gate commands.| headin skill markdown isgit worktree list | head -1inside a[ … ]test inintegrate/SKILL.md, which is not a gate and whose verdict is the test's, not the pipeline's. Nothing needed correcting — recorded here rather than presented as work done.The amendment
The rule lands in three places, none restating another:
CLAUDE.md→ Portability baseline (AC1). "A gate's output goes to a file; its exit code comes from the command, never a pipeline."cmd > "$OUT" 2>&1; rc=$?, then filter$OUT. All three recorded failure modes named, and tied back to #33: an output is trustworthy only once you have checked the status of the thing that produced it._shared/procedures/prerequisites.mdStep 1 — the runtime half. This is where every skill resolves the project's test/lint/typecheck commands, so it is where a skill actually about to run one will meet the rule. Carries the shape as a fence plus the three failure modes, because "don't pipe a gate" is only obviously right once you have seen each way it goes wrong.scripts/lint-conventions.sh— the gate (AC2). New tier-1 fence check.(Note on where the authoring rule went:
CLAUDE.mdis the operator's instructions file and this queue came from a peer session, so I did not edit it on a peer's say-so — I asked Jochem directly and he approved both edits, including keeping the Verification section's description of the linter accurate.)The linter check, and how it was verified
Two gate shapes are matched — a package manager plus a gate verb (
pnpm test,npm run lint,make build,go test,cargo test), and a test runner invoked directly or vianpx/bunx/pnpm exec(pytest,vitest,jest,tsc, …) — piped intotail|head|grep|awk|sed|less|more.The gate must sit at a command position (start of the fence line, or after
&&/||/;). Without that anchor, a runner merely named mid-line reads as a gate —grep -r vitest . | head -5was a real false positive during development, and it is exactly the kind of check that gets disabled once it cries wolf.Verified by executing, not by inspection — 18 cases:
pnpm test 2>&1 | tail -25,pnpm test | grep -E 'FAIL',npm run lint | head -40,npx vitest run 2>&1 | tail -5,go test ./... | grep FAIL,pytest tests/ -v | tail -20,make build 2>&1 | sed -n '1,20p',cargo test | less,cd apps/web && pnpm test | tail -20,npx tsc --noEmit 2>&1 | grep errorgrep -r vitest . | head -5,pnpm install | tail -5,git worktree list | head -1,jq -r .body <<<"$got1" | head -1,echo "run pytest first" | grep pytest, and the redirect-then-filter form the rule prescribespnpm test 2>&1 | tail -25fence indevelop/SKILL.mdproducedplugin/skills/develop/SKILL.md:692: gate command piped into a filterand exit 1; reverted, and the tree lints clean at 693 fence lines.Desk-check against the originating scenario
pnpm test 2>&1 | tail -25truncated past the summary, 5m 40s suite re-run$OUTand re-readable with a different filter, so there is nothing to re-runrcis the suite's, so the gate reads red. This is the one that mattered: it fed the 18-minute false alarm in #43head -40SIGPIPEd a just-started serverAll three acceptance criteria met: the rule is in the baseline and in the runtime procedure; the linter flags the shape in skill-emitted fences; and the shipped tree carried nothing to correct.
What validates it next
The linter is the validation — it runs on every meta-lane commit and the check is proven to fire. The residual risk is coverage, not correctness: the runner list is finite, so a project on a runner outside it (a bare
./gradlew checkalias, a shell wrapper) gets the prose rule without the gate. If that shows up in a dogfood run, extend the alternation rather than loosening the command-position anchor — the anchor is what keeps the check trustworthy.tea apiexits 0 on HTTP errors and prefixes NOTE: lines to stdout — audit every helper that pipes it into jq #260