0065 — One CLI grammar
--socket is one root-level clap global instead of 36 hand-copied per-verb flags; --json stays verb-scoped.
Full source summary
--socket is one root-level clap global instead of 36 hand-copied per-verb flags; --json stays verb-scoped. The root's args_conflicts_with_subcommands is replaced by explicit post-parse checks. Alias parity (ls/list, rm/remove) everywhere, one --split flag, one JSON error shape, and no -j short flag.
Status: Accepted Date: 2026-08-02
Context
The CLI grew one verb at a time and its grammar shows it: --socket was
copy-pasted 36 times (only tag had a subtree global), --json appears on
~32 verbs with no shared declaration, eight list/remove registries disagree
on alias spellings, spawn/launch/play take --split h|v while
insert-pane/move-pane take boolean --horizontal/--vertical (and
discarded --horizontal), and a --json verb failing against a dead socket
printed prose to stderr with nothing machine-readable anywhere. Each copy is
a drift risk and every inconsistency is a lesson an agent or user has to
learn twice.
Decision
--socketis a true clap global on the rootCli— one declaration,global = true; all per-verb copies (includingtag’s subtree global,service install’s field, and theagent/configaction copies) are deleted.phux --socket X lsandphux ls --socket Xare the same invocation. Verbs that never dial a server (pair,plugin,remote,relay,satellite,enroll,completion,logs, the localconfigandworkspace inspectactions, most ofservice) refuse a provided--socketwith a one-line teaching error (commands::socketless_verb).args_conflicts_with_subcommandsis removed from the root. The planning premise “clap globals parse both positions with zero teaching machinery” is false while that setting is on: clap 4.5 errors on any matched root arg followed by a subcommand, with no exemption for global args (clap_builder 4.5.44parser.rs:480/:530). Its one job — keeping the root--recpair on the nakedphuxattach — moves to an explicit post-parse check that refuses--rec/--rec-formatalongside any subcommand, namingphux attach --recandphux recas the remedies.--jsonstays verb-scoped. Help stays honest by construction: a global--jsonwould advertise itself on verbs that cannot honor it. The scoped-flag rationale that used to justify per-verb--socketcopies is kept for--recand--json, and inverted for--socket(see Why). A shared flattenedJsonOptstruct unifies the declaration (task D2). Misplacedphux --json lsgets aCli::try_parseinterception: clap’s refusal plus a hint to place the flag after the verb, produced for any long flag that exists on some verb in the tree.- JSON error contract. Codifies the shape
spatial.rsalready emits, extended: one line of JSON on stderr —{"schema_version": N, "error": {"code", "message"}, "remedy", "exit_code"}— with stdout empty and exit codes unchanged (0 success, 1 miss/no server, 2 refusal/usage, 3 partial view, 124/125 timeouts). Rolled out by tasks D2/D3. - Alias parity. Every list/remove registry gets
ls+listandrm+removeas visible aliases;plugin unlinkgainsrm/remove;taggains--json;launch --liststays a flag (it filters a verb, not a registry). Rolled out by task D3. --splitunification.insert-pane/move-paneadopt--split(value enum,h/valiases) with the booleans hidden-deprecated for one release;service install --quicunifies toSocketAddrto matchserver --quic. Rolled out by task D4.- No
-jshort flag for--json— considered and rejected. The binary has 10 short flags total, all high-frequency human-typed (-o,-s,-c,-n,-f,-e).--jsonis overwhelmingly typed by scripts and agents, where explicitness is worth more than two saved characters and nothing is retyped interactively. Adopting-jon ~32 verbs would quadruple the short-flag surface for zero scripting gain and spend the letter forever. Revisit only with evidence of interactive use.
Why
The per-verb copies optimized for honest help but produced drift: three
different --socket doc strings, one subtree accidentally global, and a
flag missing from newer verbs. For a flag that (a) means the same thing
everywhere, (b) is consumed by 36 of 41 verbs, and (c) is set once per
environment rather than per call, the drift cost outweighs the honesty
cost — so --socket inverts to global while --rec and --json, which
are genuinely per-verb semantics, stay scoped. The socketless teaching
error restores the honesty the global gives up: the flag parses everywhere,
but a verb that cannot honor it says so instead of shrugging.
Tradeoffs
phux pair --helpnow shows a--socketit will refuse at runtime; the refusal message is the compensation.- Post-parse checks are code where a clap setting used to be declaration;
both checks are pinned by regression tests
(
root_rec_before_a_verb_is_refused_post_parse,socketless_verbs_are_named_and_socket_consumers_are_not). attach --quic/--wsversus--socketexclusion moved from a clapconflicts_withto a runtime check, because clap validates conflicts per parser and a root-matched--socketnever meets a sub-matched--quic.- Deprecated boolean split flags linger hidden for a release.
Alternatives
Keep per-verb --socket copies, add a lint. A grep-based CI check could
pin the 36 copies to one spelling, but it cannot make phux --socket X ls
parse, which is the position every other multiplexer accepts.
Make --json global too. Symmetric, but wrong: a third of the surface
has no JSON projection, and clap cannot express per-verb requires against
a root global (the same per-parser limit as the conflicts above).
Keep args_conflicts_with_subcommands and special-case --socket in a
pre-parse argv rewrite. Reordering argv before clap sees it hides the real
grammar from --help, completions, and every future maintainer.
Adopt -j. Rejected above (Decision 7); recorded here so the audit
finding has a deliberate answer rather than an omission.