Skip to content

session-selectors

One selector grammar (id·id-prefix·node·branch) and one matcher, so every session command names the same sessions.

raw source

A user names a session several ways — its full id, a short id-prefix, its node, or its branch — and every session command should understand the SAME names. The bug this node removes: the list verbs (ls / watch / wait / the graph) matched on all four, but the control verbs (review / merge / send / close / reopen / capture / prompt) took a RAW id straight to the backend's exact-match endpoint, so a prefix / node / branch selector worked for ls but NOT for merge — forcing callers to hand-resolve full ids. One grammar, one matcher, no per-command matching logic anywhere.

expanded spec

One predicate. matchesSelector(session, q) is the single rule: q matches a session iff it is the session's full id, an id-PREFIX, its node, or its branch. Nothing re-derives it — both shapes below call it, so the grammar can never drift between "which sessions does ls show" and "which session does merge act on".

A selector may be a comma list. q is split on commas and matches iff ANY part names the session — the same a,b convention as --status, so spex watch a,b and spex watch a b are equivalent. A single name is just the one-part case. This closes a silent failure: before, watch a,b was one literal selector that matched no session at all (an id/node/branch never contains a comma), so a comma-joined watch streamed zero events forever with no error — exactly the trap a --status-trained user falls into.

Two shapes over the one predicate. selectSessions is the MANY shape — the list / stream / graph filter (graph, spex ls): empty selectors (or @all) means everything, with an optional status filter on top. resolveSession is the ONE shape — the single-target lookup the control verbs need. Its result is DISCRIMINATED so a caller fails precisely: ok (one match), ambiguous (a prefix or node hitting several — carried so the user can be told which), none (nothing). An exact full-id match wins outright, so a full id is never reported ambiguous merely because it prefixes a longer one.

Every control verb routes through it. Because the backend's /api/sessions/:id matches the id EXACTLY, each control verb resolves the selector FIRST — against the live board, via remote-client's resolveClientSession — and then calls with the resolved FULL id. The CLI turns a non-ok result into a precise error and a non-zero exit (none → no such session; ambiguous → the candidates). So the read verbs and the control verbs share one selector grammar, and no command carries its own matching.

The local state producers (session done / park / ask, and review-as-propose) are deliberately NOT here: they never NAME another session at all. Each resolves the agent's OWN session by id — the --session <id> the hooks pass from the payload, else the harness env var (ownSessionId) — and writes (or, for review-as-propose, reads by exact id) that session's GLOBAL record directly (state), so it must work with no backend up. There is no selector to match here, and never a lookup against the backend board.