feat(cli): simplify the machine contract before the freeze #85

Merged
natsukium merged 5 commits from feat/cli-machine-contract-23 into main 2026-09-04 21:00:03 +09:00
Owner

Fixes #23.

Freezes the CLI's machine contract before v0.1 by removing its exceptions
rather than adding capability:

  • sessions list becomes a point verb answering {"v":1,"sessions":[…]};
    --format jsonl is now a usage error there.
  • Every refusal clap can state is stated to clap; the one post-parse refusal
    left (a global carrier flag on a window-addressed verb) speaks the verb's
    own --format framing under the new usage kind, and a machine format
    silences the console log so a point verb's stderr is its one error object.
  • The exit code becomes a function of ErrorKind: a typed refusal exits 1,
    a failure to ask at all exits 2. Transport stalls report daemon_lost,
    keeping timeout for waits a caller asked for.
  • short_id leaves every reply that saw no roster (an unchecked uniqueness
    claim); list and info keep it, shortened against the roster they carry.
  • The retarget destination is --to-host, matching --to-socket; the
    felis ssh alias is gone.
  • One wire reason maps to one error.kind on every stream verb.

Doc cascade: docs/reference/cli.md, docs/reference/control-surfaces.md,
docs/reference/ipc.md, docs/reference/spec.md,
docs/explanation/architecture/control-surfaces.md,
docs/explanation/architecture/ipc.md,
docs/explanation/architecture/session-lifecycle.md, the affected how-to
pages, README.md, CHANGELOG.md, and the product-shipped skills/felis.

Verified: just check green (fmt, clippy, nextest, deny); reviewed by
plan/correctness/docs lenses and pi sol.

Deferred

  • Replace parse_point_format / parse_stream_format / parse_region_source
    with clap ValueEnums so help, completions and man pages list possible
    values — no acceptance criterion needs it (it is #55's review note), and it
    would add ~150 lines of enum, conversion and test to a change already at the
    size ceiling.
  • A protocol / unsupported row in the golden failure matrix (a pinned
    minor-4 fixture daemon) — the plan itself allows skipping it; the harness
    cannot pin an older negotiated minor without a new daemon fixture.
  • Plan I's suggested tripwires (a render_long_help() assertion that
    felis ssh is gone, a __mangen test that the page set equals the visible
    subcommand set) — both are listed as "consider";
    the_ssh_alias_no_longer_resolves already pins the parse side, and the
    man-page set follows clap with no separate list to drift from.
  • Rewriting the pre-existing em-dashed sentences the prose checker exempts on
    untouched lines of cli.md / control-surfaces.md — out of scope; the
    checker only governs added lines and reports 0 hits.
  • ipc.md states normative answers for an operation-lookup query no wire
    message can ask — the ledger's queryability belongs to the ledger feature
    (#24-adjacent), not to this issue's criteria.
  • The unreleased changelog's older entries still describe the felis ssh
    alias, --host as a retarget destination, list as a JSONL stream, a
    closed error.kind, and rosterless short_id — the triage plan scoped
    changelog handling to a single "Removed" entry; every cited line predates
    this branch and stands as history.
Fixes #23. Freezes the CLI's machine contract before v0.1 by removing its exceptions rather than adding capability: - `sessions list` becomes a **point** verb answering `{"v":1,"sessions":[…]}`; `--format jsonl` is now a usage error there. - Every refusal clap can state is stated to clap; the one post-parse refusal left (a global carrier flag on a window-addressed verb) speaks the verb's own `--format` framing under the new `usage` kind, and a machine format silences the console log so a point verb's stderr is its one error object. - The exit code becomes a function of `ErrorKind`: a typed refusal exits 1, a failure to ask at all exits 2. Transport stalls report `daemon_lost`, keeping `timeout` for waits a caller asked for. - `short_id` leaves every reply that saw no roster (an unchecked uniqueness claim); `list` and `info` keep it, shortened against the roster they carry. - The retarget destination is `--to-host`, matching `--to-socket`; the `felis ssh` alias is gone. - One wire reason maps to one `error.kind` on every stream verb. Doc cascade: `docs/reference/cli.md`, `docs/reference/control-surfaces.md`, `docs/reference/ipc.md`, `docs/reference/spec.md`, `docs/explanation/architecture/control-surfaces.md`, `docs/explanation/architecture/ipc.md`, `docs/explanation/architecture/session-lifecycle.md`, the affected how-to pages, `README.md`, `CHANGELOG.md`, and the product-shipped `skills/felis`. Verified: just check green (fmt, clippy, nextest, deny); reviewed by plan/correctness/docs lenses and pi sol. ## Deferred - Replace `parse_point_format` / `parse_stream_format` / `parse_region_source` with clap `ValueEnum`s so help, completions and man pages list possible values — no acceptance criterion needs it (it is #55's review note), and it would add ~150 lines of enum, conversion and test to a change already at the size ceiling. - A `protocol` / `unsupported` row in the golden failure matrix (a pinned minor-4 fixture daemon) — the plan itself allows skipping it; the harness cannot pin an older negotiated minor without a new daemon fixture. - Plan I's suggested tripwires (a `render_long_help()` assertion that `felis ssh` is gone, a `__mangen` test that the page set equals the visible subcommand set) — both are listed as "consider"; `the_ssh_alias_no_longer_resolves` already pins the parse side, and the man-page set follows clap with no separate list to drift from. - Rewriting the pre-existing em-dashed sentences the prose checker exempts on untouched lines of `cli.md` / `control-surfaces.md` — out of scope; the checker only governs added lines and reports 0 hits. - `ipc.md` states normative answers for an operation-lookup query no wire message can ask — the ledger's queryability belongs to the ledger feature (#24-adjacent), not to this issue's criteria. - The unreleased changelog's older entries still describe the `felis ssh` alias, `--host` as a retarget destination, `list` as a JSONL stream, a closed `error.kind`, and rosterless `short_id` — the triage plan scoped changelog handling to a single "Removed" entry; every cited line predates this branch and stands as history.
The CLI's machine surface froze avoidable exceptions into shapes a
consumer would have to carry past the first tag: a bounded roster was
framed as a stream, a usage error after a successful parse was human
whatever `--format` said, and every exit code was a per-site literal,
so one condition exited 1 on one verb and 2 on another.

Five changes, each removing an exception rather than adding a
capability:

- `sessions list` becomes a point verb answering `{"sessions":[…]}`.
  The roster is bounded and arrives in one frame, and `felis bridge`
  already answered it as one object, so the stream framing described
  no source anyone had.
- Every refusal clap can state is stated to clap (`--ssh-arg` requires
  its carrier, `--session` conflicts with a trailing command override,
  `send`'s payload is required unless `--wait` / `--key` stands in).
  That leaves one post-parse refusal, a global carrier flag on a verb
  addressed to a window, which clap cannot express; it now speaks the
  verb's framing under the new `usage` kind. A machine format also
  silences the console log, so a point verb's stderr is its one error
  object and nothing else.
- The exit code becomes a function of `ErrorKind`: a typed refusal
  exits 1, a failure to ask at all exits 2. The transport stalls that
  shared the `timeout` token report `daemon_lost` instead, keeping
  `timeout` for the waits a caller asked for.
- `short_id` leaves every reply that saw no roster. A prefix nothing
  checked is an unchecked claim of uniqueness, and a consumer reading
  one field on two kinds of reply cannot tell the cases apart.
- The retarget destination is `--to-host`, matching `--to-socket`, and
  the `felis ssh` alias is gone: one operation spelled two ways gave
  one destination three spellings, and the alias's positional
  destination is what kept `--ssh-arg`'s requirement out of clap.

Refs #23
The wire comments described the cross-carrier re-dial as `felis ssh`,
a spelling the CLI no longer has, so a reader of `felis.proto` or the
client's push handling would look for a verb that is not there. The
generated prost output is regenerated for the same reason; no field,
number, or name moves.

Refs #23
The reference pages still described the old surface: `list` as a
stream, `short_id` on every reply, `error.kind` as a closed set, the
`felis ssh` alias as permanent. The explanation twin still argued for
it. Each decision is now recorded where it is owned: the class
boundary (source, not answer shape), where a refusal of the invocation
lands, the kind-to-code table, the additive vocabulary, and the
rejected alias with its revisit trigger. The `jq` idioms in the how-to
pages open the roster array instead of dropping a terminal that no
longer exists, and `skills/felis` carries the same facts, since a
stale shipped skill teaches an agent a surface felis no longer has.

Refs #23
fix(cli): give one wire reason one kind on every stream verb
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 47s
windows / cargo nextest (Windows) (pull_request) Failing after 1m47s
windows / frontend smoke (Windows) (pull_request) Has been skipped
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m36s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m8s
pr / cargo build / clippy / test / deny (pull_request) Failing after 1m30s
pr / nix flake check (pull_request) Successful in 38s
pr / wire schema is compatible with the base (pull_request) Successful in 10s
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
4b22c3d087
The freeze's point of a single kind -> exit-code table was that a
condition answers the same way whichever verb met it, and three
surfaces still disagreed after it landed.

`capture` and `search` collapsed every `StreamErrorReason` to
`refused`, so an uncompilable `search --regex` exited 1 as `refused`
where the reference promises `invalid_request`, and the daemon's own
`Internal` fault -- a broken peer, the exit-2 class -- read as a typed
refusal. `notifications subscribe` split `InvalidRequest` off but
collapsed the rest, so the same frame produced different answers on
different verbs. The mapping now lives beside the exit-code table and
every stream verb reads its failing terminal through it.

The help text, the reference and the changelog kept describing the
surface as it was before this branch: `short_id` fields on replies
that no longer carry any, `--host` as the retarget destination after
the rename to `--to-host`, `sessions list --format jsonl` after `list`
became a point verb, `error.kind` as a closed set in the bridge
section that the same page had already made additive, the `felis ssh`
alias in an entry whose own batch deletes it, and `config path` at
exit 1 where the kind table now says 2. Each is a statement a script
author would act on and be wrong.

The failure matrix gained the protocol row acceptance criterion 2
names: a peer that completes the handshake and then answers the roster
query with a window-only push. It is the class that would have caught
the mis-mapped stream reasons above.

Refs #23
natsukium force-pushed feat/cli-machine-contract-23 from 4b22c3d087
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 47s
windows / cargo nextest (Windows) (pull_request) Failing after 1m47s
windows / frontend smoke (Windows) (pull_request) Has been skipped
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m36s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m8s
pr / cargo build / clippy / test / deny (pull_request) Failing after 1m30s
pr / nix flake check (pull_request) Successful in 38s
pr / wire schema is compatible with the base (pull_request) Successful in 10s
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
to 62be87be9d
All checks were successful
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 47s
pr / wire schema is compatible with the base (pull_request) Successful in 8s
windows / cargo clippy (Windows cross) (pull_request) Successful in 13s
bench / Criterion regression gate (pull_request) Successful in 2m5s
windows / cargo nextest (Windows) (pull_request) Successful in 6m5s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m2s
pr / nix flake check (pull_request) Successful in 25s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m37s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 47s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / frontend smoke (Windows) (pull_request) Successful in 1m57s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (push) Has been skipped
darwin / build felis (aarch64-darwin) (push) Successful in 13s
fuzz / cargo fuzz smoke (per target) (push) Successful in 1m1s
pr / nix flake check (push) Successful in 5s
pr / cargo build / clippy / test / deny (push) Successful in 1m33s
pr / wire schema is compatible with the base (push) Successful in 10s
pr / frontend smoke (x86_64-linux) (push) Successful in 6s
windows / cargo clippy (Windows cross) (push) Successful in 11s
pr / publish felis (x86_64-linux) (push) Successful in 9s
windows / cargo nextest (Windows) (push) Successful in 7m14s
windows / frontend smoke (Windows) (push) Successful in 1m41s
windows / package felis (x86_64-pc-windows-msvc) (push) Successful in 1m35s
2026-09-04 20:45:35 +09:00
Compare
natsukium deleted branch feat/cli-machine-contract-23 2026-09-04 21:00:03 +09:00
Sign in to join this conversation.
No description provided.