gc bd list: a repeated --status silently keeps only the LAST one, so the natural way to write a union returns a confident empty answer #21

Open
opened 2026-09-07 20:22:32 +00:00 by forgejo-admin · 0 comments

What happens

gc bd list --status open --status in_progress does not return the union. It returns only the beads matching the last --status given. Every earlier one is discarded with no warning, no error, and exit 0.

Measured 2026-09-07 against the celilo store, same assignee, three consecutive rounds, identical results each time:

gc bd list --rig celilo --assignee celilo/gastown.refinery --status open                       => 1
gc bd list --rig celilo --assignee celilo/gastown.refinery --status in_progress                => 0
gc bd list --rig celilo --assignee celilo/gastown.refinery --status open --status in_progress  => 0     <-- should be 1

Order dependence confirms the mechanism rather than leaving it inferred:

--status open --status in_progress   => 0     (uses in_progress)
--status in_progress --status open   => 1     (uses open)
--status closed --status open        => 1     (uses open)
--status open --status closed        => 130   (uses closed)

The last flag wins in all four.

Why this is worse than an ordinary flag quirk

The failure is silent and the result is well formed. There is no error, the exit code is 0, and the output is valid JSON. A caller gets [] and cannot distinguish "nothing matches" from "your filter was thrown away".

It also fails in the direction of the intent. Nobody writes two --status flags meaning "only the second one". They write it meaning "either of these". So the command reliably returns a narrower answer than the one asked for, at the exact moment the caller was trying to widen it.

And an empty answer is the most convincing kind of wrong answer, because it usually confirms what you already believed. gc-scratch's own CLAUDE.md carries a section on this shape ("a gc/bd reporting command answers about a SMALLER set than you asked for, and says nothing about the difference"), listing three prior instances. This is a fourth, and the first where the discarded input is a flag the user typed on the command line rather than a default they did not know about.

What it cost

The gc-scratch mayor used this exact invocation to read the celilo refinery's queue depth, twice in one session, and reported "refinery queue: empty" both times. The queue was not empty: ce-2k87 (hook-jail-config-surface slice 1, P1) was sitting in it, pushed, with branch / target=main / merge_strategy=direct set correctly and waiting for the merge gate.

Reading a merge queue as empty is a bad thing to be wrong about. It is the input to "is anything waiting on the refinery", which is the input to whether the mayor nudges it. A confident empty here means finished work sits unmerged while every surface says there is nothing to do, which is the same stranded-but-healthy-looking shape as ce-2qm.

Suggested fix

Accumulate repeated --status into a set and filter on membership, which is what every caller writing the flag twice already expects. If accumulating is not wanted, reject the second occurrence with an error naming the conflict. Either is fine. Silently keeping the last one is the only option that cannot be detected by the caller.

A comma form (--status open,in_progress) would also work, but it should be added alongside fixing the repeat rather than instead of it, because the repeated form is what people type and it will keep being typed.

Workaround in use

Query by the other fields and filter status client-side, which does return the full set:

gc bd query --rig celilo --json 'assignee=celilo/gastown.refinery' | jq '[.[]|select(.status!="closed")]'

Filed from gc-scratch. No bead: no polecat in that city can change the gc binary.

## What happens `gc bd list --status open --status in_progress` does not return the union. It returns only the beads matching the **last** `--status` given. Every earlier one is discarded with no warning, no error, and exit 0. Measured 2026-09-07 against the celilo store, same assignee, three consecutive rounds, identical results each time: ``` gc bd list --rig celilo --assignee celilo/gastown.refinery --status open => 1 gc bd list --rig celilo --assignee celilo/gastown.refinery --status in_progress => 0 gc bd list --rig celilo --assignee celilo/gastown.refinery --status open --status in_progress => 0 <-- should be 1 ``` Order dependence confirms the mechanism rather than leaving it inferred: ``` --status open --status in_progress => 0 (uses in_progress) --status in_progress --status open => 1 (uses open) --status closed --status open => 1 (uses open) --status open --status closed => 130 (uses closed) ``` The last flag wins in all four. ## Why this is worse than an ordinary flag quirk The failure is silent and the result is well formed. There is no error, the exit code is 0, and the output is valid JSON. A caller gets `[]` and cannot distinguish "nothing matches" from "your filter was thrown away". It also fails in the direction of the intent. Nobody writes two `--status` flags meaning "only the second one". They write it meaning "either of these". So the command reliably returns a **narrower** answer than the one asked for, at the exact moment the caller was trying to widen it. And an empty answer is the most convincing kind of wrong answer, because it usually confirms what you already believed. gc-scratch's own `CLAUDE.md` carries a section on this shape ("a `gc`/`bd` reporting command answers about a SMALLER set than you asked for, and says nothing about the difference"), listing three prior instances. This is a fourth, and the first where the discarded input is a flag the user typed on the command line rather than a default they did not know about. ## What it cost The gc-scratch mayor used this exact invocation to read the celilo refinery's queue depth, twice in one session, and reported "refinery queue: empty" both times. The queue was not empty: `ce-2k87` (hook-jail-config-surface slice 1, P1) was sitting in it, pushed, with `branch` / `target=main` / `merge_strategy=direct` set correctly and waiting for the merge gate. Reading a merge queue as empty is a bad thing to be wrong about. It is the input to "is anything waiting on the refinery", which is the input to whether the mayor nudges it. A confident empty here means finished work sits unmerged while every surface says there is nothing to do, which is the same stranded-but-healthy-looking shape as ce-2qm. ## Suggested fix Accumulate repeated `--status` into a set and filter on membership, which is what every caller writing the flag twice already expects. If accumulating is not wanted, reject the second occurrence with an error naming the conflict. Either is fine. Silently keeping the last one is the only option that cannot be detected by the caller. A comma form (`--status open,in_progress`) would also work, but it should be added **alongside** fixing the repeat rather than instead of it, because the repeated form is what people type and it will keep being typed. ## Workaround in use Query by the other fields and filter status client-side, which does return the full set: ``` gc bd query --rig celilo --json 'assignee=celilo/gastown.refinery' | jq '[.[]|select(.status!="closed")]' ``` Filed from gc-scratch. No bead: no polecat in that city can change the gc binary.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
celilo/gascity#21
No description provided.