Problem
A Row 2 key has one refused face, and it is used for every refusal. The
operator cannot tell a guard working correctly from a key that is broken.
Observed live. The operator pressed ISSUE repeatedly and reported: "i press
issue, i see a line underneath the word issue but nothing else." That line is
the refused marker. Nothing on the key said why, and the two candidate
causes were indistinguishable without reading a log file:
19:33:37 send: armed issue for 2feedd52 -- press again to confirm
19:33:49 send: refusing issue -- target is working, and a confirm verb must fire while the operator is watching
19:34:09 send: no such verb: stop
19:34:25 send: refusing issue -- target is working, and a confirm verb must fire while the operator is watching
Three materially different situations, one identical face:
- the guard is working — the target is busy, so an outward-facing verb
refuses (bin/fleet-send, the WAKEABLE_STATES check);
- nothing is configured — the key has no verb set in its property
inspector (plugin/src/plugin.ts, the !verb branch);
- the verb does not exist — no file for that id, e.g.
stop, which is on
the panel but not yet written.
This matters more than a normal polish issue because the whole premise of the
panel is that state is legible at a glance. A key that says only "no" on a
deck whose job is answering "what is going on" is the one place that premise
breaks. The operator's next move differs completely between the three: wait,
configure the key, or write a verb file.
Ruled out
Not a rendering bug and not a stale plugin. During diagnosis the deck was
running a stale bundle (Stream Deck had started at 13:17, the build was 15:14)
which separately suppressed the ARMED face — that has been fixed by restarting
the app, and the refusal ambiguity remains afterwards. The information all
exists in ~/.fleet/fleet.log; it simply never reaches the key.
What a fix has to decide
bin/fleet-send currently reports three outcomes by exit code: 0 delivered,
1 refused, 2 armed. Distinguishing refusals means widening that channel,
and there is a real choice about how far:
- More exit codes (
3 busy, 4 unconfigured, 5 no-such-verb). Cheapest,
keeps the plugin dumb, but the exit code becomes a growing enum that both
sides must agree on, and it does not carry any detail.
- Structured output on stdout —
fleet-send prints a small JSON verdict
the plugin parses. Extensible and carries a reason string, but fleet-send
currently prints nothing, and giving it stdout means being careful it never
becomes a hook-style contract violation elsewhere.
- Only distinguish "not now" from "no" — one extra face for a
transient/busy refusal, everything else stays as today. Smallest change that
fixes the operator's actual confusion, at the cost of still merging
unconfigured with unknown-verb.
Whichever is chosen, the second half is visual: refused currently renders as
a bright bar. A "not now" state should read as transient rather than wrong —
and it must not borrow the attention amber, which already means ARMED on this
row.
Not in scope
The never-queue rule that produced the busy refusal is being changed
separately (confirm verbs will be allowed to queue briefly against a working
agent). That reduces how often the busy refusal appears; it does not make the
refusal legible, and every other refusal path is untouched.
Problem
A Row 2 key has one
refusedface, and it is used for every refusal. Theoperator cannot tell a guard working correctly from a key that is broken.
Observed live. The operator pressed ISSUE repeatedly and reported: "i press
issue, i see a line underneath the word issue but nothing else." That line is
the
refusedmarker. Nothing on the key said why, and the two candidatecauses were indistinguishable without reading a log file:
Three materially different situations, one identical face:
refuses (
bin/fleet-send, theWAKEABLE_STATEScheck);inspector (
plugin/src/plugin.ts, the!verbbranch);stop, which is onthe panel but not yet written.
This matters more than a normal polish issue because the whole premise of the
panel is that state is legible at a glance. A key that says only "no" on a
deck whose job is answering "what is going on" is the one place that premise
breaks. The operator's next move differs completely between the three: wait,
configure the key, or write a verb file.
Ruled out
Not a rendering bug and not a stale plugin. During diagnosis the deck was
running a stale bundle (Stream Deck had started at 13:17, the build was 15:14)
which separately suppressed the ARMED face — that has been fixed by restarting
the app, and the refusal ambiguity remains afterwards. The information all
exists in
~/.fleet/fleet.log; it simply never reaches the key.What a fix has to decide
bin/fleet-sendcurrently reports three outcomes by exit code:0delivered,1refused,2armed. Distinguishing refusals means widening that channel,and there is a real choice about how far:
3busy,4unconfigured,5no-such-verb). Cheapest,keeps the plugin dumb, but the exit code becomes a growing enum that both
sides must agree on, and it does not carry any detail.
fleet-sendprints a small JSON verdictthe plugin parses. Extensible and carries a reason string, but
fleet-sendcurrently prints nothing, and giving it stdout means being careful it never
becomes a hook-style contract violation elsewhere.
transient/busy refusal, everything else stays as today. Smallest change that
fixes the operator's actual confusion, at the cost of still merging
unconfigured with unknown-verb.
Whichever is chosen, the second half is visual:
refusedcurrently renders asa bright bar. A "not now" state should read as transient rather than wrong —
and it must not borrow the attention amber, which already means ARMED on this
row.
Not in scope
The never-queue rule that produced the busy refusal is being changed
separately (confirm verbs will be allowed to queue briefly against a working
agent). That reduces how often the busy refusal appears; it does not make the
refusal legible, and every other refusal path is untouched.