[v0.1/CLI Review] remote daemon auto-spawn 契約の矛盾を解消する #141

Closed
opened 2026-09-05 11:51:46 +09:00 by natsukium · 2 comments
Owner

背景

v0.1.0 の公開契約レビューで、remote daemon の auto-spawn 方針について normative reference 同士の矛盾を確認した。

docs/reference/cli.md の scripting contract は次のように断言している。

Over --host, remote daemons are never spawned automatically.

一方、実装と他の設計文書は intent-based policy を採用している。

  • sessions spawn と window launch は local/remote のどちらでも daemon を auto-spawn する。
  • read/drive verbs は local では cold socket を失敗にし、remote では felis-daemon relay --no-spawn を使う。
  • docs/reference/ipc.md(Cross-host carrier / CLI clients)と docs/explanation/architecture/control-surfaces.md はこの挙動を明記している。
  • crates/felis-cli/src/conn.rsDial::OpsOrSpawnReconnector::dial_launch を通り、SSH carrier では relay に spawn を許可する。
  • crates/felis-daemon/src/relay.rs--no-spawn がない場合 connect_or_spawn_persistent を実行する。

つまり現状では、felis --host remote sessions spawnfelis --host remote は cold remote daemon を起動するが、CLI の公開 scripting contract は起動しないと約束している。

なぜ release blocker か

これは単なる説明上の typo ではなく、スクリプトが remote host に永続プロセスを作るかどうかという副作用の契約である。最初の release 後にどちらかへ揃えると、既存 automation の lifecycle assumption を壊す。

現在の intent split(spawn/window は起動、read/drive は起動しない)は control-surface rationale と整合しているため、第一候補は実装を維持して cli.md を修正すること。ただし release 前に明示的に決定し、テストで固定する必要がある。

提案

docs/reference/cli.md の auto-spawn 項目を、carrier ではなく verb intent が決める契約へ修正する。

  • local/remote 共通: sessions spawn と window launches は auto-spawn を許可。
  • local/remote 共通: read/drive verbs、observer、bridge、status/version probe は cold daemon を起動しない。
  • remote の no-spawn は relay command に --no-spawn を付けることで実現する。

Acceptance criteria

  • docs/reference/cli.mddocs/reference/ipc.mddocs/explanation/architecture/control-surfaces.md が同じ per-verb policy を記述する。
  • local/SSH carrier × spawn/window/read/drive の policy matrix をテストし、SSH read/drive だけでなく observer/bridge/probe も relay --no-spawn になることを確認する。
  • SSH の sessions spawn と window launch は relay--no-spawn を付けないことをテストする。
  • felis sessions spawn --help / root help / scripting reference から、remote side effect を予測できる。
  • user-facing change として必要なら CHANGELOG.mdskills/felis を cascade する。
  • #111 は relay 自身の real auto-spawn と process-group detach をテストする issue であり、CLI caller が verb ごとに --no-spawn を選ぶ公開 policy matrix は対象外。
  • #42 は dynamic completion を local-only にした issue。completion が remote daemon を起動しない決定はこの matrix に含める。
  • #55 は CLI argument grammar review。本件は argument parsing ではなく、parse 後の carrier side effect と normative docs の矛盾。

Parent: #12 / #31

Sources

  • docs/reference/cli.md:306-309
  • docs/reference/ipc.md:93-101, 1946-1951
  • docs/explanation/architecture/control-surfaces.md:291-303
  • crates/felis-cli/src/conn.rs:43-86
  • crates/felis-client-core/src/dial.rs:118-131
  • crates/felis-client-core/src/connector.rs:318-331
  • crates/felis-daemon/src/relay.rs:1-7, 87-100
## 背景 v0.1.0 の公開契約レビューで、remote daemon の auto-spawn 方針について normative reference 同士の矛盾を確認した。 `docs/reference/cli.md` の scripting contract は次のように断言している。 > Over `--host`, remote daemons are never spawned automatically. 一方、実装と他の設計文書は intent-based policy を採用している。 - `sessions spawn` と window launch は local/remote のどちらでも daemon を auto-spawn する。 - read/drive verbs は local では cold socket を失敗にし、remote では `felis-daemon relay --no-spawn` を使う。 - `docs/reference/ipc.md`(Cross-host carrier / CLI clients)と `docs/explanation/architecture/control-surfaces.md` はこの挙動を明記している。 - `crates/felis-cli/src/conn.rs` の `Dial::OpsOrSpawn` は `Reconnector::dial_launch` を通り、SSH carrier では relay に spawn を許可する。 - `crates/felis-daemon/src/relay.rs` は `--no-spawn` がない場合 `connect_or_spawn_persistent` を実行する。 つまり現状では、`felis --host remote sessions spawn` や `felis --host remote` は cold remote daemon を起動するが、CLI の公開 scripting contract は起動しないと約束している。 ## なぜ release blocker か これは単なる説明上の typo ではなく、スクリプトが remote host に永続プロセスを作るかどうかという副作用の契約である。最初の release 後にどちらかへ揃えると、既存 automation の lifecycle assumption を壊す。 現在の intent split(spawn/window は起動、read/drive は起動しない)は control-surface rationale と整合しているため、第一候補は実装を維持して `cli.md` を修正すること。ただし release 前に明示的に決定し、テストで固定する必要がある。 ## 提案 `docs/reference/cli.md` の auto-spawn 項目を、carrier ではなく verb intent が決める契約へ修正する。 - local/remote 共通: `sessions spawn` と window launches は auto-spawn を許可。 - local/remote 共通: read/drive verbs、observer、bridge、status/version probe は cold daemon を起動しない。 - remote の no-spawn は relay command に `--no-spawn` を付けることで実現する。 ## Acceptance criteria - [ ] `docs/reference/cli.md`、`docs/reference/ipc.md`、`docs/explanation/architecture/control-surfaces.md` が同じ per-verb policy を記述する。 - [ ] local/SSH carrier × spawn/window/read/drive の policy matrix をテストし、SSH read/drive だけでなく observer/bridge/probe も `relay --no-spawn` になることを確認する。 - [ ] SSH の `sessions spawn` と window launch は `relay` に `--no-spawn` を付けないことをテストする。 - [ ] `felis sessions spawn --help` / root help / scripting reference から、remote side effect を予測できる。 - [ ] user-facing change として必要なら `CHANGELOG.md` と `skills/felis` を cascade する。 ## Related - #111 は relay 自身の real auto-spawn と process-group detach をテストする issue であり、CLI caller が verb ごとに `--no-spawn` を選ぶ公開 policy matrix は対象外。 - #42 は dynamic completion を local-only にした issue。completion が remote daemon を起動しない決定はこの matrix に含める。 - #55 は CLI argument grammar review。本件は argument parsing ではなく、parse 後の carrier side effect と normative docs の矛盾。 Parent: #12 / #31 ## Sources - `docs/reference/cli.md:306-309` - `docs/reference/ipc.md:93-101`, `1946-1951` - `docs/explanation/architecture/control-surfaces.md:291-303` - `crates/felis-cli/src/conn.rs:43-86` - `crates/felis-client-core/src/dial.rs:118-131` - `crates/felis-client-core/src/connector.rs:318-331` - `crates/felis-daemon/src/relay.rs:1-7`, `87-100`
Author
Owner

Triage (2026-09-05)

Verdict: accept, P1. Verified: docs/reference/cli.md:306-309 promises no remote auto-spawn while conn.rs Dial::OpsOrSpawn and relay.rs without --no-spawn do spawn. Keep the implementation (verb intent decides, carrier does not) and fix the reference, as proposed. The policy matrix test (local/SSH × spawn/window/read/drive/observer/bridge/probe) is the acceptance gate; #42's completion rule belongs in the same matrix. Parent: #55.

## Triage (2026-09-05) **Verdict: accept, P1.** Verified: `docs/reference/cli.md:306-309` promises no remote auto-spawn while `conn.rs` `Dial::OpsOrSpawn` and `relay.rs` without `--no-spawn` do spawn. Keep the implementation (verb intent decides, carrier does not) and fix the reference, as proposed. The policy matrix test (local/SSH × spawn/window/read/drive/observer/bridge/probe) is the acceptance gate; #42's completion rule belongs in the same matrix. Parent: #55.
Author
Owner

Triage plan (2026-09-05)

Source-grounded triage against main at 07a77ae4, reviewed through six rounds of an independent reviewer (pi luna then sol); every finding was verified against the source and folded in, and the design decisions below were settled with the maintainer on 2026-09-05. The order that supersedes the tracker's is posted on #12: this issue lands before #29 (schemas, goldens, and reference tables freeze there).

Remote auto-spawn: fix the reference, pin the verb → dial matrix

Claim check

Confirmed. docs/reference/cli.md:306-309 says "Over --host, remote
daemons are never spawned automatically"; the code and the other two
normative docs say intent decides:

  • crates/felis-cli/src/conn.rs:43-86 Dial::{Ops, Observer, OpsOrSpawn};
    OpsOrSpawnReconnector::dial_launch (felis-client-core/src/dial.rs:118-134)
    RemoteSpawn::Allow over SSH; Ops/Observerconn.rs:114-116
    RemoteSpawn::Refuserelay --no-spawn (connector.rs:328).
  • Dial sites: sessions spawn OpsOrSpawn (cli_sessions.rs:641); every
    other sessions verb Ops (:529, :888); daemon status Ops
    (cli_daemon.rs:55); notifications subscribe Observer
    (cli_notifications.rs:118); window launches RemoteSpawn::Allow
    (dial.rs:269); dynamic completions are local-only (#42).
  • docs/reference/ipc.md:93-101 and
    docs/explanation/architecture/control-surfaces.md:291-303 already
    state the split.

Verdict

accept as proposed. Keep the implementation; the reference sentence is
the bug.

Approach

  • docs/reference/cli.md "The scripting contract" → "Auto-spawning": one
    rule for both carriers: sessions spawn and window launches spawn; every
    read/drive verb, notifications subscribe, daemon status, version,
    doctor, felis bridge, and completions do not. Retargets are two
    dials: the source dial (window retarget, sessions retarget-all)
    never spawns, and the landing the receiving window then makes on the
    target host goes through dial_and_land
    (felis-client-core/src/dial.rs:260-328, RemoteSpawn::Allow).
    RemoteSpawn governs only the SSH relay: connect_carrier
    (connector.rs:522-548) calls local::connect directly for a local
    carrier and never connect_or_spawn_daemon. So the target-side
    landing spawns over SSH (attach and create alike) and does not spawn
    on a cold local socket — that retarget fails. Document exactly that
    split as its own two rows (SSH target: spawns; local target: cold
    socket fails) rather than "one rule for both carriers", and leave the
    local behavior as-is: making a local retarget spawn is new behavior,
    not a doc fix, and belongs to a separate issue if wanted; over SSH the non-spawning forms
    dial felis-daemon relay --no-spawn. Add the --no-spawn sentence to
    "Alternate carriers" if that section names the relay command.
  • skills/felis/SKILL.md: the same sentence wherever it describes remote
    use. CHANGELOG.md: not needed (documentation of existing behavior).
  • Test: pin the spawn policy separately from the connection mode, since
    Dial bundles both and the bridge picks its Offer per operation
    (cli_bridge.rs:2877-2882, always through conn::dial = Refuse). Make
    each verb's spawn policy (RemoteSpawn::Allow/Refuse) a const on
    its handler and add one test enumerating every verb (bridge included,
    as Refuse with operation-dependent offers) with its expected policy,
    and, for the retarget landing, two tests: the SSH relay command carries
    no --no-spawn (RemoteSpawn::Allow through dial_and_land), and a
    local cold socket yields a connect error with no daemon spawned;
    the existing relay_command tests (connector.rs:1324-1342) already
    prove Allow → no flag and Refuse--no-spawn.

Dependencies / risk / labels

None. S. Keep labels. Parent #55.

## Triage plan (2026-09-05) Source-grounded triage against `main` at `07a77ae4`, reviewed through six rounds of an independent reviewer (`pi` luna then sol); every finding was verified against the source and folded in, and the design decisions below were settled with the maintainer on 2026-09-05. The order that supersedes the tracker's is posted on #12: this issue lands before #29 (schemas, goldens, and reference tables freeze there). **Remote auto-spawn: fix the reference, pin the verb → dial matrix** ### Claim check Confirmed. `docs/reference/cli.md:306-309` says "Over `--host`, remote daemons are never spawned automatically"; the code and the other two normative docs say intent decides: - `crates/felis-cli/src/conn.rs:43-86` `Dial::{Ops, Observer, OpsOrSpawn}`; `OpsOrSpawn` → `Reconnector::dial_launch` (`felis-client-core/src/dial.rs:118-134`) → `RemoteSpawn::Allow` over SSH; `Ops`/`Observer` → `conn.rs:114-116` `RemoteSpawn::Refuse` → `relay --no-spawn` (`connector.rs:328`). - Dial sites: `sessions spawn` `OpsOrSpawn` (`cli_sessions.rs:641`); every other sessions verb `Ops` (`:529`, `:888`); `daemon status` `Ops` (`cli_daemon.rs:55`); `notifications subscribe` `Observer` (`cli_notifications.rs:118`); window launches `RemoteSpawn::Allow` (`dial.rs:269`); dynamic completions are local-only (#42). - `docs/reference/ipc.md:93-101` and `docs/explanation/architecture/control-surfaces.md:291-303` already state the split. ### Verdict **accept as proposed.** Keep the implementation; the reference sentence is the bug. ### Approach - `docs/reference/cli.md` "The scripting contract" → "Auto-spawning": one rule for both carriers: `sessions spawn` and window launches spawn; every read/drive verb, `notifications subscribe`, `daemon status`, `version`, `doctor`, `felis bridge`, and completions do not. Retargets are two dials: the source dial (`window retarget`, `sessions retarget-all`) never spawns, and the *landing* the receiving window then makes on the target host goes through `dial_and_land` (`felis-client-core/src/dial.rs:260-328`, `RemoteSpawn::Allow`). `RemoteSpawn` governs only the SSH relay: `connect_carrier` (`connector.rs:522-548`) calls `local::connect` directly for a local carrier and never `connect_or_spawn_daemon`. So the target-side landing spawns over SSH (attach and create alike) and does *not* spawn on a cold local socket — that retarget fails. Document exactly that split as its own two rows (SSH target: spawns; local target: cold socket fails) rather than "one rule for both carriers", and leave the local behavior as-is: making a local retarget spawn is new behavior, not a doc fix, and belongs to a separate issue if wanted; over SSH the non-spawning forms dial `felis-daemon relay --no-spawn`. Add the `--no-spawn` sentence to "Alternate carriers" if that section names the relay command. - `skills/felis/SKILL.md`: the same sentence wherever it describes remote use. `CHANGELOG.md`: not needed (documentation of existing behavior). - Test: pin the spawn policy separately from the connection mode, since `Dial` bundles both and the bridge picks its `Offer` per operation (`cli_bridge.rs:2877-2882`, always through `conn::dial` = Refuse). Make each verb's spawn policy (`RemoteSpawn::Allow`/`Refuse`) a `const` on its handler and add one test enumerating every verb (bridge included, as Refuse with operation-dependent offers) with its expected policy, and, for the retarget landing, two tests: the SSH relay command carries no `--no-spawn` (`RemoteSpawn::Allow` through `dial_and_land`), and a local cold socket yields a connect error with no daemon spawned; the existing `relay_command` tests (`connector.rs:1324-1342`) already prove `Allow` → no flag and `Refuse` → `--no-spawn`. ### Dependencies / risk / labels None. **S.** Keep labels. Parent #55.
Sign in to join this conversation.
No description provided.