cli: reinstate felis ssh <dest>, drop the all-mirror verbs and the --to-* destination flags #277

Closed
opened 2026-09-14 00:35:20 +09:00 by natsukium · 0 comments
Owner

Reinstate felis ssh <dest> as the verb that re-points the current window at a remote daemon, make
window retarget the local-only form with a positional destination, and remove the all-mirror verbs
and the --to-* destination flags. Nothing is tagged (git tag is empty, no Forgejo release), so the
surface is redesigned in place rather than deprecated.

Decision

  • A window is an attachment. window retarget acts on the frontend attachment the command runs in
    (session-lifecycle.md "Attached: one or more same-user clients subscribe to grid updates and send
    input"). The session never moves; the window re-attaches elsewhere. That reading stands.
  • felis ssh <dest> is the SSH form of that verb. "ssh somewhere" is the phrase a user reaches
    for, and its everyday meaning is "point this terminal at that host", not "open a new window there".
    The rejected-alias record (docs/explanation/architecture/control-surfaces.md:546-552) is
    superseded. Its two objections do not survive the new shape: the clap objection existed only because
    the alias shared window retarget's parser (--ssh-arg could not require a destination one
    spelling supplied positionally); a separate subcommand with a required positional destination makes
    --ssh-arg unconditional. The "one destination, three spellings" objection disappears because
    --to-host is removed: the SSH destination has exactly one spelling.
  • sessions switch-all and sessions retarget-all are removed. They are the pre-B-9 broadcast
    kept under a new name when the per-window default landed (97a7a755: "The broadcast keeps its
    behavior under …"). No consumer or scenario was ever recorded for moving every mirror at once, and the
    cross-carrier form does not even preserve what it claims to move: with a Create landing each
    window dials independently and lands on its own fresh session, so the mirrors come apart on arrival.
    Principle 1, second Test: "if no real consumer needs it yet, the terminal does not implement it."
    The one scenario the history names, "a script sweeping several sessions onto another host"
    (f513f60d), is the rationale of a test pin, not a consumer; such a sweep is a loop over
    window retarget --from <id> per session. The bridge's sessions.switch parameter all
    (cli_bridge.rs:723, :1203-1229, docs/reference/cli.md:563, schemas/felis-bridge-v1.schema.json)
    is the same capability under another spelling and goes with the verbs.
    The removal earns no decision record in the docs: doc-cascade §3 "Default to no record" applies, and the
    why lives in the commit body and the CHANGELOG.md Removed entry. No Revisit if line is written; §3
    permits one, the maintainer chose not to record one. Decided 2026-09-14.
  • --to-host / --to-socket are removed. The to- prefix existed only to keep the verb's
    destination from reading as the root --host / --socket (control-surfaces.md:554). With the SSH
    destination positional on felis ssh, the local destination becomes positional on window retarget
    and the prefix has nothing left to disambiguate.

Resulting grammar

felis ssh <dest> [--ssh-arg <TOKEN>]... [--attachment <id>] [--from <prefix>] [--session <prefix> | -- <cmd>...]
felis window retarget [<socket-path>] [--attachment <id>] [--from <prefix>] [--session <prefix> | -- <cmd>...]
  • <dest> is the OpenSSH destination, verbatim (user@host, ssh://…, a config alias). Required.
  • <socket-path> is a local daemon socket. Absent means the default local daemon: the way home after
    felis ssh, spelled as the absence of a choice (the existing "no --local flag" rule stays).
  • --ssh-arg lives on felis ssh only; no requires, no post-parse refusal.
  • The root carrier (felis --host a ssh b) stays a usage error; that rule is unchanged.
  • felis ssh devbox (re-point this window) and felis --host devbox (open a new window there) are
    different verbs for different operations; the reference and the how-to must show them side by side.

Wire

SwitchScope.all (felis.proto:1356) loses its producers (CLI verbs and the bridge's all) and goes with
them: a wire variant nothing can reach is not a capability. Decided 2026-09-14:

  • An absent OpsSwitch.scope is malformed and is refused at decode (convert/ops.rs:204-214 today maps
    absence to All). Absence must never initiate movement, and Default would silently turn an
    unversioned request into a one-window move. Add the decode-rejection test beside the other hand-built
    wire rejections in convert/.
  • oneof tag 3 is reserved, never recycled (docs/reference/ipc.md rule on retired numbers).
  • This is an intended pre-release wire break: just proto, then just proto-compat with a
    base: <merge-base sha> line and its why in crates/felis-protocol/proto/BREAKING.md. Not a minor bump.
  • The minor-2 ledger row (docs/reference/ipc.md:1539, preface.rs MINOR_LEDGER) is rewritten to
    describe the current design: SwitchScope is Default / Attachment(id); a peer that sends no scope is
    refused. The check_scope_minor gates in the CLI and bridge follow.
  • The daemon's all-mirror push path in session_task.rs and its tests go with it.

Cascade

Code (extend-ipc):

  • crates/felis-cli/src/cli_bridge.rs: drop the all parameter from both sessions.switch parse paths and
    the allowed-parameter table; cli_schema.rs SessionsSwitchParams.all; the "all": true fixture in
    crates/felis-cli/tests/schema_fixtures.rs; bridge tests; just schema regenerates
    schemas/felis-bridge-v1.schema.json; docs/reference/cli.md:563 follows.
  • The NoInputOwner diagnostics (cli_sessions.rs:1583-1586, cli_bridge.rs:1746-1748) stop recommending
    the -all form / all parameter and name --attachment / attachment alone; pin the new text in tests.
  • Compatibility gates: check_scope_minor in crates/felis-client-core/src/connector.rs:199-210 and
    SWITCH_SCOPE_FIELDS in crates/felis-protocol/src/minor.rs:191-197 both treat All as the minor-0
    representation; with All gone every scope requires minor 2, and both gates plus their tests change.
  • crates/felis-cli/src/main.rs, cli_sessions.rs: add the Ssh subcommand, drop RetargetAll,
    SwitchAll, RetargetCarrierFlags (to_host, to_socket), make the window retarget destination
    positional; RETARGET_ALL_VERB and the shared-grammar test retarget_all_speaks_the_window_retarget_grammar go.
  • cli_completions.rs (fish/zsh/bash tokens for retarget-all, switch-all, --to-host),
    cli_doctor.rs, the help snapshot every_visible_help_page_is_snapshotted.snap, tests.rs,
    crates/felis-cli/tests/cli_sessions.rs.
  • crates/felis-daemon/src/serve/session_task.rs: the all-mirror push path and its tests
    (all_mirror_retarget_push_reaches_only_window_subscribers and siblings).
  • crates/felis-protocol/proto/felis.proto (SwitchScopeAll, reserved 3), messages.rs,
    convert/ops.rs, regenerated felis.v1.rs, proto/BREAKING.md, preface.rs ledger;
    docs/reference/ipc.md (minor-2 row, SwitchScope shape) follows.

Docs (doc-cascade):

  • docs/explanation/architecture/control-surfaces.md: rewrite the window namespace section
    (:540-575, :625-634) to describe the current design only, with the felis ssh rationale and no
    reference to the alias, the -all verbs, or the to- prefix. History is not doc content (§4).
  • docs/reference/cli.md (:92, :200-201, :236-237, :263-264, :432-452, :478, :492, :508),
    docs/reference/control-surfaces.md, docs/reference/ipc.md, docs/reference/keybindings.md.
  • docs/how-to/attach-over-ssh.md, docs/how-to/drive-a-session-beside-its-window.md.
  • skills/felis/SKILL.md (product-shipped; the retarget verbs move from "surfaces this skill does not
    cover" or stay there, but the spelling it names must be the new one), README.md if it names any of
    the verbs.
  • CHANGELOG.md under [Unreleased]: Added felis ssh <dest>; Changed window retarget [<socket-path>];
    Removed sessions switch-all, sessions retarget-all, --to-host, --to-socket. The dated [0.1.0]
    section is left as written even though the tag is not pushed.
  • Grep sweep before done: retarget-all, switch-all, to-host, to_host, to-socket, to_socket,
    felis ssh, all-mirror, SwitchScopeAll, SwitchScope::All, -all and "all" in crates/felis-cli/src,
    across docs/, skills/, crates/, README.md.

Acceptance

  • felis ssh devbox re-points the invoking window at devbox's daemon; felis window retarget brings it back.
  • felis ssh with no destination is a clap usage error (exit 2); felis window retarget --to-host x,
    felis sessions retarget-all, felis sessions switch-all are unknown to clap.
  • No file under docs/, skills/, or README.md names retarget-all, switch-all, --to-host,
    --to-socket, or the bridge all parameter; control-surfaces.md describes the two retarget verbs as they
    are and carries no rejection record or Revisit if for the removed forms.
  • An OpsSwitch with no scope is refused as malformed, pinned by a decode test; just proto-compat passes
    with the BREAKING.md acknowledgment.
  • just check passes; the help snapshot is regenerated, not patched.

Related: #275 (the split-daemon bug that surfaced this discussion; independent of it).

Reinstate `felis ssh <dest>` as the verb that re-points the current window at a remote daemon, make `window retarget` the local-only form with a positional destination, and remove the all-mirror verbs and the `--to-*` destination flags. Nothing is tagged (`git tag` is empty, no Forgejo release), so the surface is redesigned in place rather than deprecated. ## Decision - **A window is an attachment.** `window retarget` acts on the frontend attachment the command runs in (`session-lifecycle.md` "Attached: one or more same-user clients subscribe to grid updates and send input"). The session never moves; the window re-attaches elsewhere. That reading stands. - **`felis ssh <dest>` is the SSH form of that verb.** "ssh somewhere" is the phrase a user reaches for, and its everyday meaning is "point *this* terminal at that host", not "open a new window there". The rejected-alias record (`docs/explanation/architecture/control-surfaces.md:546-552`) is superseded. Its two objections do not survive the new shape: the clap objection existed only because the alias shared `window retarget`'s parser (`--ssh-arg` could not `require` a destination one spelling supplied positionally); a separate subcommand with a required positional destination makes `--ssh-arg` unconditional. The "one destination, three spellings" objection disappears because `--to-host` is removed: the SSH destination has exactly one spelling. - **`sessions switch-all` and `sessions retarget-all` are removed.** They are the pre-B-9 broadcast kept under a new name when the per-window default landed (`97a7a755`: "The broadcast keeps its behavior under …"). No consumer or scenario was ever recorded for moving every mirror at once, and the cross-carrier form does not even preserve what it claims to move: with a `Create` landing each window dials independently and lands on its own fresh session, so the mirrors come apart on arrival. Principle 1, second Test: "if no real consumer needs it yet, the terminal does not implement it." The one scenario the history names, "a script sweeping several sessions onto another host" (`f513f60d`), is the rationale of a test pin, not a consumer; such a sweep is a loop over `window retarget --from <id>` per session. The bridge's `sessions.switch` parameter `all` (`cli_bridge.rs:723`, `:1203-1229`, `docs/reference/cli.md:563`, `schemas/felis-bridge-v1.schema.json`) is the same capability under another spelling and goes with the verbs. The removal earns no decision record in the docs: doc-cascade §3 "Default to no record" applies, and the why lives in the commit body and the `CHANGELOG.md` Removed entry. No *Revisit if* line is written; §3 permits one, the maintainer chose not to record one. Decided 2026-09-14. - **`--to-host` / `--to-socket` are removed.** The `to-` prefix existed only to keep the verb's destination from reading as the root `--host` / `--socket` (`control-surfaces.md:554`). With the SSH destination positional on `felis ssh`, the local destination becomes positional on `window retarget` and the prefix has nothing left to disambiguate. ## Resulting grammar ``` felis ssh <dest> [--ssh-arg <TOKEN>]... [--attachment <id>] [--from <prefix>] [--session <prefix> | -- <cmd>...] felis window retarget [<socket-path>] [--attachment <id>] [--from <prefix>] [--session <prefix> | -- <cmd>...] ``` - `<dest>` is the OpenSSH destination, verbatim (`user@host`, `ssh://…`, a config alias). Required. - `<socket-path>` is a local daemon socket. Absent means the default local daemon: the way home after `felis ssh`, spelled as the absence of a choice (the existing "no `--local` flag" rule stays). - `--ssh-arg` lives on `felis ssh` only; no `requires`, no post-parse refusal. - The root carrier (`felis --host a ssh b`) stays a usage error; that rule is unchanged. - `felis ssh devbox` (re-point this window) and `felis --host devbox` (open a new window there) are different verbs for different operations; the reference and the how-to must show them side by side. ## Wire `SwitchScope.all` (`felis.proto:1356`) loses its producers (CLI verbs and the bridge's `all`) and goes with them: a wire variant nothing can reach is not a capability. Decided 2026-09-14: - **An absent `OpsSwitch.scope` is malformed** and is refused at decode (`convert/ops.rs:204-214` today maps absence to `All`). Absence must never initiate movement, and `Default` would silently turn an unversioned request into a one-window move. Add the decode-rejection test beside the other hand-built wire rejections in `convert/`. - **oneof tag 3 is `reserved`**, never recycled (`docs/reference/ipc.md` rule on retired numbers). - **This is an intended pre-release wire break**: `just proto`, then `just proto-compat` with a `base: <merge-base sha>` line and its why in `crates/felis-protocol/proto/BREAKING.md`. Not a minor bump. - **The minor-2 ledger row** (`docs/reference/ipc.md:1539`, `preface.rs` `MINOR_LEDGER`) is rewritten to describe the current design: `SwitchScope` is `Default` / `Attachment(id)`; a peer that sends no scope is refused. The `check_scope_minor` gates in the CLI and bridge follow. - The daemon's all-mirror push path in `session_task.rs` and its tests go with it. ## Cascade Code (`extend-ipc`): - `crates/felis-cli/src/cli_bridge.rs`: drop the `all` parameter from both `sessions.switch` parse paths and the allowed-parameter table; `cli_schema.rs` `SessionsSwitchParams.all`; the `"all": true` fixture in `crates/felis-cli/tests/schema_fixtures.rs`; bridge tests; `just schema` regenerates `schemas/felis-bridge-v1.schema.json`; `docs/reference/cli.md:563` follows. - The `NoInputOwner` diagnostics (`cli_sessions.rs:1583-1586`, `cli_bridge.rs:1746-1748`) stop recommending the `-all` form / `all` parameter and name `--attachment` / `attachment` alone; pin the new text in tests. - Compatibility gates: `check_scope_minor` in `crates/felis-client-core/src/connector.rs:199-210` and `SWITCH_SCOPE_FIELDS` in `crates/felis-protocol/src/minor.rs:191-197` both treat `All` as the minor-0 representation; with `All` gone every scope requires minor 2, and both gates plus their tests change. - `crates/felis-cli/src/main.rs`, `cli_sessions.rs`: add the `Ssh` subcommand, drop `RetargetAll`, `SwitchAll`, `RetargetCarrierFlags` (`to_host`, `to_socket`), make the `window retarget` destination positional; `RETARGET_ALL_VERB` and the shared-grammar test `retarget_all_speaks_the_window_retarget_grammar` go. - `cli_completions.rs` (fish/zsh/bash tokens for `retarget-all`, `switch-all`, `--to-host`), `cli_doctor.rs`, the help snapshot `every_visible_help_page_is_snapshotted.snap`, `tests.rs`, `crates/felis-cli/tests/cli_sessions.rs`. - `crates/felis-daemon/src/serve/session_task.rs`: the all-mirror push path and its tests (`all_mirror_retarget_push_reaches_only_window_subscribers` and siblings). - `crates/felis-protocol/proto/felis.proto` (`SwitchScopeAll`, `reserved 3`), `messages.rs`, `convert/ops.rs`, regenerated `felis.v1.rs`, `proto/BREAKING.md`, `preface.rs` ledger; `docs/reference/ipc.md` (minor-2 row, `SwitchScope` shape) follows. Docs (`doc-cascade`): - `docs/explanation/architecture/control-surfaces.md`: rewrite the `window` namespace section (`:540-575`, `:625-634`) to describe the current design only, with the `felis ssh` rationale and no reference to the alias, the `-all` verbs, or the `to-` prefix. History is not doc content (§4). - `docs/reference/cli.md` (`:92`, `:200-201`, `:236-237`, `:263-264`, `:432-452`, `:478`, `:492`, `:508`), `docs/reference/control-surfaces.md`, `docs/reference/ipc.md`, `docs/reference/keybindings.md`. - `docs/how-to/attach-over-ssh.md`, `docs/how-to/drive-a-session-beside-its-window.md`. - `skills/felis/SKILL.md` (product-shipped; the retarget verbs move from "surfaces this skill does not cover" or stay there, but the spelling it names must be the new one), `README.md` if it names any of the verbs. - `CHANGELOG.md` under `[Unreleased]`: Added `felis ssh <dest>`; Changed `window retarget [<socket-path>]`; Removed `sessions switch-all`, `sessions retarget-all`, `--to-host`, `--to-socket`. The dated `[0.1.0]` section is left as written even though the tag is not pushed. - Grep sweep before done: `retarget-all`, `switch-all`, `to-host`, `to_host`, `to-socket`, `to_socket`, `felis ssh`, `all-mirror`, `SwitchScopeAll`, `SwitchScope::All`, `-all` and `"all"` in `crates/felis-cli/src`, across `docs/`, `skills/`, `crates/`, `README.md`. ## Acceptance - `felis ssh devbox` re-points the invoking window at devbox's daemon; `felis window retarget` brings it back. - `felis ssh` with no destination is a clap usage error (exit 2); `felis window retarget --to-host x`, `felis sessions retarget-all`, `felis sessions switch-all` are unknown to clap. - No file under `docs/`, `skills/`, or `README.md` names `retarget-all`, `switch-all`, `--to-host`, `--to-socket`, or the bridge `all` parameter; `control-surfaces.md` describes the two retarget verbs as they are and carries no rejection record or *Revisit if* for the removed forms. - An `OpsSwitch` with no `scope` is refused as malformed, pinned by a decode test; `just proto-compat` passes with the `BREAKING.md` acknowledgment. - `just check` passes; the help snapshot is regenerated, not patched. Related: #275 (the split-daemon bug that surfaced this discussion; independent of it).
Sign in to join this conversation.
No description provided.