workflow-fix: empty selection over local artifacts must fail loud, not exit rc=0
Highlight text on any card (body, plan, or an event) to anchor a comment, or leave a whole-task comment here. Mention @claude to summon a reply.
No comments yet.
Overview / Motivation
Auto-filed by the workflow-fix-on-bug protocol from a workflow-fix candidate raised on task #1739 (emitting agent: the #1739 orchestrator session, 2026-08-05).
Task #1739's evil-ood-spread-round hit ONE bug class THREE times in a single round: a selection/filter predicate written against an assumed artifact schema selects ZERO rows from an already-local committed artifact, and the script treats the empty set as a successful no-op and exits rc=0. Two of the three burned real time (one produced a plausible-looking output file that read as a completed run and was misdiagnosed as a missing deliverable for two days); the third was caught only by an ad-hoc dry-run minutes before a ~160,000-call Batch API wave.
Goal
Establish a fail-loud convention for empty post-selection row sets over local committed artifacts — an empty selection must raise, never exit rc=0 as a silent no-op — so this class is caught at the selecting script rather than after the compute is spent.
Workflow gap
- Bug observed (three instances, same round):
scripts/issue1739_trait_rejudge.py(2026-08-03, recorded at #1739epm:progressv412): CLI rung names vs on-disktrain/hhrt/toxicchat— leg died; fixed by adding_LOCAL_RUNG_ALIAS(commit0d09a51f49).scripts/issue1739_rescore_ood.py_load_plain_ladder_cells: filtered top-levelf_u/u_rung_labelwhile the committedpercell/cells.jsonlcarries that identity nested insideunit_key— kept 0 of 826 cells and wroteood_detection_metrics.jsonwithn_metric_rows: 0at rc=0. Measured fails-pre-fix at round 19: old filter 0/826, new filter 270/826. This one also cost two days of misdiagnosis, because the empty output file looked like a completed run.scripts/issue1739_compliance_full.py_load_rollouts_local: same rung-alias mismatch as (1), carried in a pre-0d09a51f49copy — would have judged 0 of 159,990 requests at rc=0. Caught pre-launch by a zero-API dry-run; fixed by porting_LOCAL_RUNG_ALIAS(commit086d2588fe).
- Why it is a workflow gap: the recurring cost is not the individual typo — it is that an empty selection is INDISTINGUISHABLE FROM SUCCESS. Every instance exited rc=0, and instance (2) additionally emitted a well-formed artifact. Round 19 added exactly the right guard (raise on empty cell selection AND on a 0-metric-row result set) but only to ONE site, so the next site re-learns it. Three occurrences in one round is the signal that this belongs in the always-consulted rule surface rather than in per-script patches.
- Distinct from the existing nearest rule — this is a SIBLING, not a duplicate.
.claude/rules/gotchas.mdalready carries the #1092 real-corpus streaming-filter entry ("a filter chain written from assumed field shapes can reject 100% of rows while every synthetic-fixture smoke stays green", verified present: 1 hit forreject 100% of rows), and that entry even records #1739's own Reddit-dump string-boolean shapes. But its SCOPE is remote streaming corpus builders and its REMEDY is a bounded tiny-real streaming probe with per-filter reject counters. The class filed here differs on both axes: the input is an ALREADY-LOCAL COMMITTED artifact (judge rollout mirrors, per-cell JSONL, cell registries) where no streaming probe applies, and the remedy is a fail-loud non-empty-selection assertion at the selection site. The new entry should cross-reference the #1092 one rather than restate it. - Confidence (emitter): high — three reproducible instances with measured fails-pre-fix evidence for two of them, all in current-tree scripts.
- verified-at-filing: per-target
grep -cat body-compose time 2026-08-05 —.claude/rules/gotchas.md:reject 100% of rows= 1 (the sibling entry, must exist),empty selection|non-empty-selection|selected zero rows= 0 (the claimed gap, absent);.claude/rules/code-style.md: same gap pattern = 0. All three incident scripts confirmed present onissue-1739HEAD viagit cat-file -e. Recently-closed siblings ongotchas.mdin the last 7 days reviewed (5 commits: #2079 sha-pin lint, #2073 autocompact window, #2058 spec sync, the probed-tool-traps pair, #2050 pgrep-f) — none covers empty-selection semantics, so this is not a re-raise of a just-landed fix.
Proposed change (candidate diff sketch — refine in planning)
- Add a
.claude/rules/gotchas.mdentry, sibling to and cross-referencing the #1092 streaming-filter entry: any selection/filter/join step over a local committed artifact must fail loud on an empty result set — an empty post-filter row set, an empty cell selection, or a zero-row result table is aRuntimeError, never a silent rc=0 no-op — and must NOT write a well-formed "success" artifact (instance 2'sn_metric_rows: 0file is the trap). State the three #1739 instances as the evidence and name the two recurring mismatch shapes: CLI-vs-on-disk key aliases, and identity nested inside a JSON-string column rather than at top level. - Consider the code-side companion: a small shared helper (e.g. in
src/explore_persona_space/) that wraps "select rows, assert non-empty, report the rejecting predicate" so new scripts inherit it instead of re-deriving — the planner should judge helper-vs-convention. Whatever lands, the per-unit reject-counter idea from the #1092 entry is the right diagnostic to carry over (so a 0-selection failure names WHICH predicate emptied the set). - Do NOT weaken any existing fail-fast rule; this only adds a case.
Scope / surfaces
- Primary target:
.claude/rules/gotchas.md. - Secondary (planner's call):
.claude/rules/code-style.mdfail-fast section, and/or a shared selection helper undersrc/explore_persona_space/. - Evidence-only, do not edit:
scripts/issue1739_{trait_rejudge,rescore_ood,compliance_full}.py(all three already fixed in-round).
Constraints / invariants
- Workflow-surface only for the rule text; a shared helper would be library code and is in scope only if the planner elects it.
- Must not contradict the #1092 streaming entry — cross-reference, do not restate or supersede.
scripts/workflow_lint.py --check-lessons-indexpasses if a rule row is added/renamed; ruff clean on any touched code; the gotchas size ratchet respected.
Provenance
- workflow_fix_target: .claude/rules/gotchas.md
- fingerprint: empty-selection-local-artifact-silent-rc0
Raised as prose by the #1739 orchestrator after the third instance in one round was caught minutes before a ~160k-call Batch API wave; the correction + full three-instance table are recorded in an epm:progress marker on #1739 (2026-08-05).