diff --git a/docs/migration-to-agend-terminal.md b/docs/migration-to-agend-terminal.md new file mode 100644 index 00000000..d630efd2 --- /dev/null +++ b/docs/migration-to-agend-terminal.md @@ -0,0 +1,246 @@ +# Migrating from AgEnD (TS) to agend-terminal (Rust) + +> [!IMPORTANT] +> This guide is the canonical migration reference for moving an existing +> `@suzuke/agend` (TypeScript) install to +> [`agend-terminal`](https://github.com/suzuke/agend-terminal) (Rust). It is +> built incrementally — Sprint 2 Phase A landed Sections 2 + 3 first because +> CLI flags and `fleet.yaml` schema are the highest-friction surface for +> existing operators. Other sections fill in over Phases B and C. + +> [!WARNING] +> `agend-terminal` is a Rust rewrite, not a port. The `fleet.yaml` schema is +> deliberately leaner than the TS one — many per-instance knobs that lived in +> the YAML have moved to env vars, backend presets, or per-backend instruction +> files. **Read Section 3 carefully before copying your existing config.** + +## Why migrate? {#why-migrate} + +*(Phase C — covers: motivation, agend-terminal feature delta over @suzuke/agend, when not to migrate, supported migration window.)* + +## CLI flag mapping {#cli-flag-mapping} + +`agend-terminal` exposes a **single flat command list** (see [`src/main.rs:165-337`](https://github.com/suzuke/agend-terminal/blob/main/src/main.rs#L165-L337)) rather than the multi-group structure (`agend fleet …`, `agend backend …`, …) you are used to from `@suzuke/agend`. Most subcommand groups collapse to top-level commands; several are removed entirely (functionality moved to MCP tools, TUI overlays, env vars, or deferred). + +### Top-level commands (TS top-level → Rust) + +| `@suzuke/agend` | `agend-terminal` | Status / notes | +|---|---|---| +| `agend init` | — | **Removed.** Use `agend-terminal quickstart` for the interactive setup, or hand-author `fleet.yaml`. | +| `agend quickstart` | `agend-terminal quickstart` | ✓ Renamed equivalent. | +| `agend start` | `agend-terminal start` | ✓ Starts the daemon. Note: Rust also has `agend-terminal daemon` for explicit daemon-mode launch and `agend-terminal app` for the TUI multi-pane terminal — these are distinct on Rust side, whereas TS folds the multiplex into `start`. | +| `agend stop` | `agend-terminal stop` | ✓ | +| `agend restart` | — | **Removed.** Use `stop` + `start`. | +| `agend ls` | `agend-terminal ls` (alias of `list`) | ✓ | +| `agend health` | `agend-terminal doctor` | **Renamed.** Combines health + backend probe into one diagnostic. | +| `agend attach ` | `agend-terminal attach ` | ✓ Native PTY multiplexing replaces the TS `tmux` wrapper, so attach quit shortcuts may differ. | +| `agend logs ` | — | **Removed.** Read log files directly under `$AGEND_HOME` (`daemon.log` and per-instance log files). | +| `agend update` | `agend-terminal upgrade` (Unix-only) | **Renamed and platform-narrowed.** Hot in-place upgrade; takes `--binary `, `--yes`, `--install-supervisor`, `--stability-secs N`, `--ready-timeout-secs N`. | +| `agend reload` | — | **Removed.** Stop and restart the daemon to pick up `fleet.yaml` changes. | +| `agend install` / `agend uninstall` | — | **Removed.** Service install is delegated to OS-native tools (`systemd` / `launchd`) — the Rust binary does not register itself. | +| `agend web` | — | **Removed.** No web UI in `agend-terminal`; the TUI (`app`) replaces it. | +| `agend export` / `agend import` | — | **Removed.** No archive format yet; if you need to back up state, copy `$AGEND_HOME` directly. | +| `agend export-chat` | — | **Removed.** | +| — | `agend-terminal app` | **New.** Launches the multi-pane TUI. | +| — | `agend-terminal tray` | **New.** System-tray integration (feature-gated). | +| — | `agend-terminal inject ` | **New.** Inject a message into an instance's stdin without a Telegram round trip. | +| — | `agend-terminal kill ` | **New.** Force-kill a hung instance. | +| — | `agend-terminal connect` | **New.** Connect a controller to a running daemon. | +| — | `agend-terminal demo` | **New.** Run a guided demo flow. | +| — | `agend-terminal bugreport` | **New.** Bundle logs/config for a bug report. | +| — | `agend-terminal completions` | **New.** Print shell completions. | +| — | `agend-terminal mcp` | **New.** MCP-related diagnostics. | +| — | `agend-terminal capture` | **New.** Capture a session snapshot for postmortem. | +| — | `agend-terminal test` / `verify` | **New.** Internal verification commands. | + +### `agend fleet` group → flattened + +| TS | Rust | Status | +|---|---|---| +| `agend fleet start` | `agend-terminal fleet start [config]` | ✓ | +| `agend fleet stop` | `agend-terminal fleet stop` | ✓ | +| `agend fleet restart` | — | **Removed.** Use `fleet stop` + `fleet start`. | +| `agend fleet status` | `agend-terminal status` | **Renamed and flattened** to top-level `status`. | +| `agend fleet logs` | — | **Removed.** Read log files directly. | +| `agend fleet history` | — | **Removed.** No equivalent. | +| `agend fleet activity` | — | **Removed.** No equivalent. | +| `agend fleet cleanup` | — | **Removed.** Rust does not auto-clean stale instance dirs; delete manually if needed. | +| — | `agend-terminal admin cleanup-branches [--yes]` | **New.** Admin sub-group; cleans up stale review branches. | + +### `agend backend` group → mostly removed + +| TS | Rust | Status | +|---|---|---| +| `agend backend doctor` | `agend-terminal doctor` | **Renamed and flattened.** Same diagnostic surface; no `backend` sub-group on Rust. | +| `agend backend trust ` | — | **Removed.** Backends manage their own trust files; `agend-terminal` does not pre-approve directories. Run the backend CLI manually once to accept its trust prompt. | + +### `agend topic ...` → removed (configuration-only) + +`topic list` / `topic bind` / `topic unbind` have **no CLI equivalent** in `agend-terminal`. Topic-to-instance routing is configured in `fleet.yaml` (`instances..topic_id`); there is no runtime command to mutate the binding. + +**Migration**: edit `fleet.yaml` directly. If you need to inspect current bindings at runtime, `agend-terminal status` lists each instance's `topic_id`. + +### `agend access ...` → removed (replaced by `channel.user_allowlist`) + +`access lock` / `unlock` / `list` / `remove` / `pair` have **no CLI equivalent** in `agend-terminal`. Access control is now declarative via the top-level `channel.user_allowlist` field — see High-friction #1 in Section 3. + +**Migration**: replace runtime `access ...` mutations with edits to `channel.user_allowlist` in `fleet.yaml`, then restart the daemon. + +### `agend schedule ...` → moved to MCP tools / TUI + +`schedule list` / `add` / `update` / `delete` / `enable` / `disable` / `history` / `trigger` have **no CLI equivalent** in `agend-terminal`. Scheduling lives in two places now: + +- **MCP tools** — `create_schedule`, `list_schedules`, `update_schedule`, `delete_schedule` (callable from inside any agent's tool surface). +- **TUI overlay** — `agend-terminal app` exposes a schedule pane. + +**Migration**: scripts that called `agend schedule add` from cron/CI must invoke the daemon's MCP tool surface instead, or pre-seed schedules through `agend-terminal app`. + +## fleet.yaml schema diff {#fleet-yaml-schema-diff} + +The Rust schema lives at [`src/fleet.rs:7-183`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L7-L183) and is the source of truth for everything below. The Rust schema is **deliberately leaner** than the TS one — many per-instance knobs have moved to env vars, backend presets, or per-backend instruction files. Plan to rewrite, not just rename, when porting an existing `fleet.yaml`. + +### High-friction change #1: `user_allowlist` is fail-closed by default + +**Reference:** `agend-terminal` PR #216, schema at [`src/fleet.rs:50-60`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L50-L60), drop logic at [`src/channel/mod.rs:240-260`](https://github.com/suzuke/agend-terminal/blob/main/src/channel/mod.rs#L240-L260). + +**Field**: `channel.user_allowlist: Option>` — a top-level field on `channel`, **not nested under `access`** (Rust has no `access` sub-key on `channel`). + +**Three-state semantics:** + +| YAML | Behaviour | +|---|---| +| key absent | **Legacy open mode** — accepts all group members. Daemon logs a deprecation warning at startup; this state is supported for backwards compatibility but flagged for removal. | +| `user_allowlist: []` | **Lockdown.** Reject all senders. Useful as a kill-switch without removing the rest of the channel config. | +| `user_allowlist: [123, 456]` | **Allowlist.** Only those Telegram numeric user IDs accepted. | + +**Outbound failure mode (post-PR #216 fail-closed for outbound notify):** + +If the daemon would have notified the channel but the recipient is not allowlisted, this line is emitted to `daemon.log` at **DEBUG** level (`tracing::debug!` at `src/channel/mod.rs:251-255`): + +``` +DEBUG outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in) +``` + +> [!IMPORTANT] +> The drop event is logged at `DEBUG`, **not** `WARN`. With the default `RUST_LOG=info` you will not see it — `grep` against `daemon.log` returns nothing and the operator's natural conclusion is "config OK," which is exactly the wrong inference. **When reproducing this failure mode, set `RUST_LOG=debug` (or `RUST_LOG=agend_terminal=debug`) before starting the daemon.** A separate `agend-terminal`-side change to raise this line to `WARN` is being tracked by dev-team; this guide reflects the current behaviour. + +**Inbound failure mode:** + +Each rejected inbound is dropped with a log naming the offending `user_id`. Same caveat: confirm log level on `agend-terminal` source before relying on a specific level for grep. + +**Migration action** + +```yaml +# fleet.yaml on agend-terminal +channel: + type: telegram + bot_token_env: BOT_TOKEN + group_id: -1001234567890 # bare int, see High-friction #2 + user_allowlist: # top-level on channel; copy from channel.access.allowed_users on @suzuke/agend + - 111111111 # Telegram numeric user ID, bare int + - 222222222 +``` + +**Debugging checklist when the bot goes silent on Rust:** + +1. `grep "outbound notify dropped" $AGEND_HOME/daemon.log` — confirms the gate fired. +2. Confirm `channel.user_allowlist` is set in `fleet.yaml` and your numeric user ID is in it (use [@userinfobot](https://t.me/userinfobot) on Telegram if unsure). +3. If you previously had `channel.access.allowed_users` on `@suzuke/agend` (the fully qualified path; `access` is nested under `channel` per `src/types.ts:57`), that path is **not read** by Rust — copy the entries to top-level `channel.user_allowlist` and use bare int form for IDs. +4. **TS pairing-mode users** (those who got their user IDs onto `channel.access.allowed_users` via `agend access pair` issuing pairing codes) must also be enumerated into `channel.user_allowlist` directly — `agend-terminal` has no pairing flow equivalent. If your `@suzuke/agend` install used `access.mode: "pairing"`, list every active user ID in the Rust allowlist explicitly. + +> **Why fail-closed:** an empty/absent allowlist on a Telegram bot is a credential exposure waiting to happen — anyone who guesses or exfiltrates the bot token can DM the bot and trigger arbitrary backend tool use. Failing closed forces the operator to make an explicit access decision, which is both safer and more debuggable than silently exposing the bot. + +### High-friction change #2: `group_id` is strict `i64` — **bare int form only** + +**Reference:** [`src/fleet.rs:46`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L46) (field type) and [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) (round-trip tests). + +In `@suzuke/agend`, `channel.group_id` was typed as `number | string` and the YAML loader accepted either. **Canonical TS docs used the bare int form for Telegram supergroup IDs** — see `docs/configuration.md:23` (`group_id: -1001234567890`) and `tests/setup-wizard-config.test.ts:127` (`toBe(-1001234567890)`). The quoted-string form was reserved for **Discord guild IDs** (positive 18–19-digit snowflakes that exceed JavaScript's `Number.MAX_SAFE_INTEGER` of 2^53 − 1) — see `docs/features.md:302`, `docs/plugin-development.md:293`, `docs/plugin-adapter-architecture.md:28,298`. Quoting Discord IDs dodged JS `Number` precision loss; quoting Telegram IDs was never the canonical TS recommendation. + +In `agend-terminal`, `channel.group_id` is typed as **`i64`** with strict serde deserialization. **Only the bare int form is accepted** — a quoted-string form (`group_id: "-1001234567890"`) **fails at startup** with a serde error like `"invalid type: string \"-1001234567890\", expected i64"`. The Rust YAML parser does not auto-coerce string ↔ int. `i64` covers both Telegram negative supergroup IDs (well within range) and Discord snowflakes (which fit in 2^63 − 1) with the same bare-int form. + +Round-trip tests at [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) lock the bare-int parsing contract for representative negative IDs (`-100123456`, `-100999`, `-3`, `-1`, `-2`). Quoted-string rejection is serde's default behaviour for `i64`-typed fields and is not separately regression-tested — a future serde version with a permissive coercion knob could change it, though the typed-field contract makes such drift unlikely. + +**Migration action.** If your `fleet.yaml` quoted any `group_id` value (Discord users especially, where quoting was the standard TS workaround for the precision issue), un-quote it before the new daemon will load the config: + +```yaml +# Required form on agend-terminal (Telegram and Discord both): +channel: + group_id: -1001234567890 # bare int + +# Will fail to load on agend-terminal: +channel: + group_id: "-1001234567890" # quoted — serde rejects with "expected i64" +``` + +**Other int-vs-string parity worth knowing:** `instances..topic_id` is also strictly `Option` on Rust (fleet.rs:160) — bare int only. + +### Top-level keys + +`@suzuke/agend` `FleetConfig` (`src/types.ts:218`) → `agend-terminal` `FleetConfig` (`src/fleet.rs:7-29`): + +| TS key | Rust equivalent | Notes | +|---|---|---| +| `channel` | `channel: ChannelConfig` ✓ | Singular form. **Plus** Rust accepts plural `channels: HashMap` for multi-channel routing — `normalize()` at startup collapses `channels` to `channel` if only one entry. | +| `project_roots` | **removed** | No fleet-level allowlist of project roots; use per-instance `working_directory`. | +| `defaults` | `defaults: InstanceDefaults` ✓ | Field set differs significantly — see instance table below. | +| `instances` | `instances: HashMap` ✓ | Same role; field set leaner. | +| `teams` | `teams: HashMap` ✓ | Rust shape (`src/fleet.rs:175-183`): `{ members, orchestrator?, description? }`. The new `orchestrator: Option` field names a member that acts as the routing target for team-addressed `delegate_task` and groups the team in the TUI's team-tab view. TS `TeamConfig` has only `{ members, description? }` — when porting, leave `orchestrator` unset to preserve TS-equivalent behaviour, or set it once you adopt the new routing convention. | +| `templates` | `templates: Option>` | **Parser-only on Rust today** — keys round-trip but template expansion is not implemented yet. Do not depend on TS template semantics. | +| `profiles` | **removed** | Per-instance fields are flat; reusable profiles will return as a separate concern if needed. | +| `health_port` | **removed** | Daemon does not expose a separate health port; use `agend-terminal doctor`. | +| `stt` | **removed** | Voice→cloud transcription not implemented; voice messages are not supported. | + +### `instances.` field diff + +`@suzuke/agend` `InstanceConfig` (`src/types.ts:65-111`) → `agend-terminal` `InstanceConfig` (`src/fleet.rs:140-183`): + +| TS field | Rust field (snake_case) | Migration action | +|---|---|---| +| `working_directory` | `working_directory: Option` | ✓ same. Required in practice. | +| `display_name` | `display_name: Option` | ✓ same. | +| `description` | `role: Option` (with serde alias `description`) | **Rename or use alias.** Both `role: …` and `description: …` are accepted on Rust; canonical is `role`. | +| `tags` | **removed** | Drop the field. No replacement; Rust does not use tag-based routing. | +| `topic_id` | `topic_id: Option` | ✓ same; **bare int only** (see High-friction #2). | +| `general_topic` | **removed** | Drop the field. | +| `restart_policy` | **removed** | Drop the field. Restart logic is daemon-managed via fixed constants in `health.rs` (`CRASH_WINDOW=10min`, `BACKOFF_BASE=5s`, `BACKOFF_MAX=300s`, `DEFAULT_MAX_RETRIES=5`). Per-instance restart tuning is not exposed. | +| `context_guardian` | **removed** | Drop the field. Context-rotation tuning lives in backend presets / runtime defaults. | +| `log_level` | **removed** | Drop the field. Set globally via env var (`RUST_LOG=…`). | +| `backend` | `backend: Option` | ✓ same. Backend identifier strings should match (claude / gemini / codex / opencode / kiro). | +| `tool_set` | **removed** | Drop the field. MCP tool ACL is now controlled by env vars `AGEND_MCP_TOOLS_ALLOW` / `AGEND_MCP_TOOLS_DENY` set when launching the daemon. | +| `lightweight` | **removed** | Drop the field. | +| `systemPrompt` | **removed** | Drop the field. Instructions are injected via per-backend `instructions_path` files (e.g. a workspace `CLAUDE.md`) — see the `agend-terminal` backend preset for paths. The TS `file:`-chain syntax has no Rust equivalent; concatenate manually if needed. | +| `skipPermissions` | **removed** | Drop the field. Permission flags are baked into each backend preset's `spawn_flags()`. | +| `model` | `model: Option` | ✓ same. Passed as `--model` to the backend CLI. | +| `model_failover` | **removed** | Drop the field. No automatic failover on Rust today; restart with a different model manually if the primary is rate-limited. | +| `cost_guard` | **removed** | Drop the field. No equivalent yet on Rust. | +| `worktree_source` | `git_branch: Option` (with serde alias `worktree_source`) | **Rename or use alias.** Rust canonical is `git_branch`; the TS spelling still loads via alias. | +| `workflow` | **removed** | Drop the field. | +| `startup_timeout_ms` | **removed** | Drop the field. Startup timing is governed by each backend preset's `ready_timeout_secs`. | +| `agent_mode` | **removed** | Drop the field. Communication mode (MCP vs HTTP) is implied by the backend preset; not user-tunable per instance. | + +**Rust-only fields you may want to add** when porting: + +| Rust field | Type | Purpose | +|---|---|---| +| `receive_fleet_updates` | `Option` | Default opt-in. Set `false` on instances that should not see fleet `` injections. | +| `cols`, `rows` | `Option` | Override PTY size for the instance's terminal. | +| `env` | `HashMap` | Per-instance env additions. Note: Rust filters credential-like keys per `agent.rs::SENSITIVE_ENV_KEYS` — secrets injected here may be redacted. | +| `command`, `args`, `ready_pattern` | low-level overrides | Used when the chosen backend is not a built-in preset (legacy / custom CLI). | + +**snake_case caveat:** TS schema mixes `camelCase` (`systemPrompt`, `skipPermissions`) and `snake_case` (everything else). Rust is uniformly `snake_case`. The two TS camelCase fields above are removed on Rust anyway, so this only matters for `worktree_source` / `git_branch` (alias accepts both) and `description` / `role` (alias accepts both). + +## Backend invocation diff {#backend-invocation-diff} + +*(Phase B — covers: `--mcp-config` vs Rust equivalent, `--append-system-prompt-file` flow, per-backend env-var injection, MCP server respawn semantics, fleet-instructions delivery channel — see also `docs/fleet-instructions-injection.md` on the TS side for the post-#55 model.)* + +## MCP tool API diff {#mcp-tool-api-diff} + +*(Phase B — covers: full inventory of MCP tools (~20 in TS full set), name renames, argument shape changes, return-value diffs, broadcast `cost_limited` field carry-over, deferred or removed tools.)* + +## Migration steps {#migration-steps} + +*(Phase C — covers: pre-flight checklist, `agend-terminal migrate` invocation if available, dual-run period, rollback, post-migration validation.)* + +## Known incompatibilities and deferred parity {#known-incompatibilities} + +*(Phase C — covers: features intentionally not ported, parity items deferred to a later Rust release, items still in design.)* diff --git a/docs/migration-to-agend-terminal.zh-TW.md b/docs/migration-to-agend-terminal.zh-TW.md new file mode 100644 index 00000000..6e78a1bf --- /dev/null +++ b/docs/migration-to-agend-terminal.zh-TW.md @@ -0,0 +1,244 @@ +# 從 AgEnD (TS) 遷移到 agend-terminal (Rust) + +> [!IMPORTANT] +> 本文件是把現有 `@suzuke/agend` (TypeScript) 安裝遷移到 +> [`agend-terminal`](https://github.com/suzuke/agend-terminal) (Rust) 的標準 +> migration 指引。內容會分階段補完 —— Sprint 2 Phase A 先處理 Sections 2 + 3,因為對 +> 既有 operator 來說,CLI flags 與 `fleet.yaml` schema 是阻力最高的兩個面向。其他章節 +> 會在 Phase B 與 Phase C 補上。 + +> [!WARNING] +> `agend-terminal` 是 Rust 重寫,不是直譯 port。`fleet.yaml` schema 比 TS 版精簡許多 —— +> 許多原本住在 YAML 的 per-instance 旋鈕已搬到 env vars、backend presets 或 per-backend +> 指令檔案。**搬既有 config 之前請仔細讀 Section 3。** + +## 為什麼要遷移? {#why-migrate} + +*(Phase C —— 涵蓋:遷移動機、agend-terminal 相對 @suzuke/agend 的功能差異、何時不要遷移、支援的遷移期間。)* + +## CLI flag mapping {#cli-flag-mapping} + +`agend-terminal` 採 **單一扁平指令清單** (見 [`src/main.rs:165-337`](https://github.com/suzuke/agend-terminal/blob/main/src/main.rs#L165-L337)),不再使用 `@suzuke/agend` 的多群組結構 (`agend fleet …`、`agend backend …`…)。多數子群組摺到 top-level;部分整組移除 (功能搬到 MCP tools、TUI overlay、env vars,或延後實作)。 + +### Top-level commands (TS top-level → Rust) + +| `@suzuke/agend` | `agend-terminal` | 狀態 / 備註 | +|---|---|---| +| `agend init` | — | **移除。** 改用 `agend-terminal quickstart` 互動式 setup,或手動撰寫 `fleet.yaml`。 | +| `agend quickstart` | `agend-terminal quickstart` | ✓ 等價改名。 | +| `agend start` | `agend-terminal start` | ✓ 啟動 daemon。注意:Rust 另有 `agend-terminal daemon` (顯式 daemon-mode 啟動) 與 `agend-terminal app` (TUI 多 pane 終端機介面) —— 兩者在 Rust 上是分開的指令,TS 將 multiplex 包進 `start`。 | +| `agend stop` | `agend-terminal stop` | ✓ | +| `agend restart` | — | **移除。** 用 `stop` + `start`。 | +| `agend ls` | `agend-terminal ls` (alias of `list`) | ✓ | +| `agend health` | `agend-terminal doctor` | **改名。** 把 health + backend probe 合併為一個診斷指令。 | +| `agend attach ` | `agend-terminal attach ` | ✓ 原生 PTY multiplexing 取代 TS 的 `tmux` wrapper,attach 退出快捷鍵可能不同。 | +| `agend logs ` | — | **移除。** 直接讀 `$AGEND_HOME` 下的 log 檔案 (`daemon.log` 與每個 instance 的 log)。 | +| `agend update` | `agend-terminal upgrade` (僅 Unix) | **改名且平台收斂。** Hot in-place upgrade;接收 `--binary `、`--yes`、`--install-supervisor`、`--stability-secs N`、`--ready-timeout-secs N`。 | +| `agend reload` | — | **移除。** 改 `fleet.yaml` 後 stop + start daemon。 | +| `agend install` / `agend uninstall` | — | **移除。** Service 安裝交給作業系統工具 (`systemd` / `launchd`) —— Rust binary 不自動註冊。 | +| `agend web` | — | **移除。** `agend-terminal` 沒有 web UI;TUI (`app`) 取代之。 | +| `agend export` / `agend import` | — | **移除。** 目前無 archive 格式;若要備份,直接複製 `$AGEND_HOME`。 | +| `agend export-chat` | — | **移除。** | +| — | `agend-terminal app` | **新增。** 啟動多 pane TUI。 | +| — | `agend-terminal tray` | **新增。** 系統匣整合 (feature-gated)。 | +| — | `agend-terminal inject ` | **新增。** 不透過 Telegram 直接把訊息打進 instance 的 stdin。 | +| — | `agend-terminal kill ` | **新增。** 強制 kill hung 住的 instance。 | +| — | `agend-terminal connect` | **新增。** 把 controller 接到一個運行中的 daemon。 | +| — | `agend-terminal demo` | **新增。** 跑導覽式 demo 流程。 | +| — | `agend-terminal bugreport` | **新增。** 打包 logs/config 供 bug report 使用。 | +| — | `agend-terminal completions` | **新增。** 印出 shell completions。 | +| — | `agend-terminal mcp` | **新增。** MCP 相關診斷。 | +| — | `agend-terminal capture` | **新增。** Postmortem 用 session snapshot。 | +| — | `agend-terminal test` / `verify` | **新增。** 內部驗證指令。 | + +### `agend fleet` 群組 → 摺平 + +| TS | Rust | 狀態 | +|---|---|---| +| `agend fleet start` | `agend-terminal fleet start [config]` | ✓ | +| `agend fleet stop` | `agend-terminal fleet stop` | ✓ | +| `agend fleet restart` | — | **移除。** 用 `fleet stop` + `fleet start`。 | +| `agend fleet status` | `agend-terminal status` | **改名且摺到 top-level**。 | +| `agend fleet logs` | — | **移除。** 直接讀 log 檔。 | +| `agend fleet history` | — | **移除。** 無等價物。 | +| `agend fleet activity` | — | **移除。** 無等價物。 | +| `agend fleet cleanup` | — | **移除。** Rust 不自動清 stale instance 資料夾;需要時手動刪。 | +| — | `agend-terminal admin cleanup-branches [--yes]` | **新增。** Admin 子群組;清 stale review branches。 | + +### `agend backend` 群組 → 大部分移除 + +| TS | Rust | 狀態 | +|---|---|---| +| `agend backend doctor` | `agend-terminal doctor` | **改名且摺平。** 同樣的診斷面向;Rust 無 `backend` 子群組。 | +| `agend backend trust ` | — | **移除。** 後端各自管 trust 檔案;`agend-terminal` 不預先信任目錄。改成手動跑一次 backend CLI 接受 trust 對話框。 | + +### `agend topic ...` → 移除 (僅靠設定檔) + +`topic list` / `topic bind` / `topic unbind` 在 `agend-terminal` 上 **無 CLI 等價物**。Topic-to-instance 路由改在 `fleet.yaml` (`instances..topic_id`) 設定;沒有運行時指令可改 binding。 + +**遷移**:直接編輯 `fleet.yaml`。需要查運行時 binding 改用 `agend-terminal status`,它會列每個 instance 的 `topic_id`。 + +### `agend access ...` → 移除 (改用 `channel.user_allowlist`) + +`access lock` / `unlock` / `list` / `remove` / `pair` 在 `agend-terminal` **無 CLI 等價物**。存取控制改成宣告式 —— 透過 top-level `channel.user_allowlist` 欄位 —— 見 Section 3 的 High-friction #1。 + +**遷移**:把運行時 `access ...` 操作換成編輯 `fleet.yaml` 的 `channel.user_allowlist`,然後 restart daemon。 + +### `agend schedule ...` → 搬到 MCP tools / TUI + +`schedule list` / `add` / `update` / `delete` / `enable` / `disable` / `history` / `trigger` 在 `agend-terminal` **無 CLI 等價物**。排程改住兩個地方: + +- **MCP tools** —— `create_schedule`、`list_schedules`、`update_schedule`、`delete_schedule` (任何 agent 的工具面向皆可呼叫)。 +- **TUI overlay** —— `agend-terminal app` 提供 schedule pane。 + +**遷移**:之前在 cron/CI 呼叫 `agend schedule add` 的 script 必須改呼叫 daemon 的 MCP tool 介面,或透過 `agend-terminal app` 預先輸入排程。 + +## fleet.yaml schema diff {#fleet-yaml-schema-diff} + +Rust schema 在 [`src/fleet.rs:7-183`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L7-L183),是底下所有對照的 source of truth。Rust schema **刻意比 TS 精簡** —— 許多 per-instance 旋鈕已搬到 env vars、backend presets,或 per-backend 指令檔案。Port 既有 `fleet.yaml` 時請當成「重寫」而非「重命名」。 + +### 高摩擦變更 #1:`user_allowlist` 預設 fail-closed + +**參考:** `agend-terminal` PR #216,schema 在 [`src/fleet.rs:50-60`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L50-L60),drop 邏輯在 [`src/channel/mod.rs:240-260`](https://github.com/suzuke/agend-terminal/blob/main/src/channel/mod.rs#L240-L260)。 + +**欄位**:`channel.user_allowlist: Option>` —— 是 `channel` 的 top-level 欄位,**不**巢狀於 `access` 之下 (Rust 的 `channel` 沒有 `access` 子鍵)。 + +**三態語意:** + +| YAML | 行為 | +|---|---| +| 鍵不存在 | **Legacy open mode** —— 接受所有群組成員。Daemon 啟動時記 deprecation warning;此狀態為 backwards-compat 而保留,已被標記為將來移除。 | +| `user_allowlist: []` | **Lockdown。** 拒絕所有 sender。可作為不移除其他 channel config 的 kill-switch。 | +| `user_allowlist: [123, 456]` | **Allowlist。** 只接受這些 Telegram 數字 user IDs。 | + +**Outbound 失敗模式 (post-PR #216 outbound notify fail-closed):** + +如果 daemon 想對 channel 通知,但收件者不在 allowlist 中,以下這行會以 **DEBUG** level 寫入 `daemon.log` (`tracing::debug!` 在 `src/channel/mod.rs:251-255`): + +``` +DEBUG outbound notify dropped — channel not authorised (fail-closed; configure user_allowlist to opt in) +``` + +> [!IMPORTANT] +> Drop 事件是 `DEBUG`,**不是** `WARN`。預設 `RUST_LOG=info` 下你看不到 ——`grep daemon.log` 也找不到,operator 自然會結論「config OK」,**這正好是相反的判斷**。**重現此失敗模式時請先設 `RUST_LOG=debug` (或 `RUST_LOG=agend_terminal=debug`) 再啟動 daemon。** 把這行升到 `WARN` 是 `agend-terminal` 端的修正 (dev-team backlog),本指引反映目前行為。 + +**Inbound 失敗模式:** + +每筆被拒絕的 inbound 會 drop,並記一條帶該 `user_id` 的 log。同樣注意:依特定 log level 做 grep 前,請先在 `agend-terminal` 原始碼確認 level。 + +**遷移動作** + +```yaml +# fleet.yaml on agend-terminal +channel: + type: telegram + bot_token_env: BOT_TOKEN + group_id: -1001234567890 # 裸 int,見 High-friction #2 + user_allowlist: # channel 的 top-level;從 @suzuke/agend 的 channel.access.allowed_users 複製 + - 111111111 # Telegram 數字 user ID,裸 int + - 222222222 +``` + +**Rust 上 bot 安靜時的 debug 清單:** + +1. `grep "outbound notify dropped" $AGEND_HOME/daemon.log` —— 確認 gate 觸發了。 +2. 確認 `channel.user_allowlist` 已設定於 `fleet.yaml`,且你的數字 user ID 在裡面 (不確定可在 Telegram 用 [@userinfobot](https://t.me/userinfobot))。 +3. 如果你之前在 `@suzuke/agend` 用 `channel.access.allowed_users` (完整路徑;`access` 在 `src/types.ts:57` 嵌在 `channel` 之下),該路徑 Rust **不再讀取** —— 把條目搬到 top-level `channel.user_allowlist`,IDs 用裸 int 形式。 +4. **TS pairing-mode 使用者** (透過 `agend access pair` 發 pairing 碼,使用者兌換後 ID 進到 `channel.access.allowed_users`) 也必須直接列進 `channel.user_allowlist` —— `agend-terminal` 沒有 pairing flow 等價物。如果你的 `@suzuke/agend` 環境用 `access.mode: "pairing"`,請把每個目前 active 的 user ID 明列進 Rust allowlist。 + +> **為什麼 fail-closed:** Telegram bot 上空的 / 不存在的 allowlist 是 credential 暴露的等待事故 —— 任何人猜到或外洩 bot token 後可以 DM bot 觸發任意 backend tool use。Fail-closed 強迫 operator 做出明確的存取決策,比靜默暴露更安全也更易 debug。 + +### 高摩擦變更 #2:`group_id` 嚴格 `i64` —— **僅接受裸 int** + +**參考:** [`src/fleet.rs:46`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L46) (欄位型別) 與 [`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) (round-trip 測試)。 + +在 `@suzuke/agend`,`channel.group_id` 型別是 `number | string`,YAML loader 兩種都接受。**TS canonical 文件對 Telegram supergroup ID 採裸 int** —— 見 `docs/configuration.md:23` (`group_id: -1001234567890`) 與 `tests/setup-wizard-config.test.ts:127` (`toBe(-1001234567890)`)。需要 quote 成字串的是 **Discord guild ID** (正向 18-19 位 snowflake,超過 JavaScript `Number.MAX_SAFE_INTEGER` 即 2^53 − 1) —— 見 `docs/features.md:302`、`docs/plugin-development.md:293`、`docs/plugin-adapter-architecture.md:28,298`。Quote Discord ID 是為了避開 JS `Number` 的精度損失;quote Telegram ID 從來不是 TS 的 canonical 建議。 + +在 `agend-terminal`,`channel.group_id` 嚴格定為 **`i64`**,serde deserialization 嚴格。**只接受裸 int 形式** —— quoted-string 形式 (`group_id: "-1001234567890"`) **會在啟動時失敗**,serde error 形如 `"invalid type: string \"-1001234567890\", expected i64"`。Rust YAML parser 不會自動 string ↔ int 互轉。`i64` 同時涵蓋 Telegram 負向 supergroup ID (在範圍內) 與 Discord snowflake (適合 2^63 − 1),兩者都可用同一裸 int 形式。 + +[`src/fleet.rs:725-826`](https://github.com/suzuke/agend-terminal/blob/main/src/fleet.rs#L725-L826) 的 round-trip 測試鎖定的是裸 int 解析契約,代表性負 ID 包含 (`-100123456`、`-100999`、`-3`、`-1`、`-2`)。Quoted-string 拒絕是 serde 對 `i64`-typed 欄位的預設行為,沒有單獨 regression-pinned —— 將來若 serde 出現允許寬鬆轉型的旋鈕,行為可能變,不過 typed-field 契約讓這種漂移不太可能發生。 + +**遷移動作。** 如果你的 `fleet.yaml` 對任何 `group_id` 值有 quote (尤其是 Discord 用戶,quote 是 TS 端的標準避坑作法),在新 daemon 載入 config 之前先取消 quote: + +```yaml +# agend-terminal 必要形式 (Telegram 與 Discord 皆然): +channel: + group_id: -1001234567890 # 裸 int + +# agend-terminal 載入會失敗: +channel: + group_id: "-1001234567890" # quoted —— serde 拒絕「expected i64」 +``` + +**其他值得注意的 int-vs-string parity:** `instances..topic_id` 在 Rust 也是嚴格 `Option` (fleet.rs:160) —— 僅裸 int。 + +### Top-level keys + +`@suzuke/agend` 的 `FleetConfig` (`src/types.ts:218`) → `agend-terminal` 的 `FleetConfig` (`src/fleet.rs:7-29`): + +| TS key | Rust 等價 | 備註 | +|---|---|---| +| `channel` | `channel: ChannelConfig` ✓ | 單數形式。**此外** Rust 接受複數形式 `channels: HashMap` 用於 multi-channel routing —— 啟動時的 `normalize()` 會在只有一個 entry 時把 `channels` 摺成 `channel`。 | +| `project_roots` | **移除** | 沒有 fleet-level 的 project root allowlist;改用 per-instance `working_directory`。 | +| `defaults` | `defaults: InstanceDefaults` ✓ | 欄位集合差異很大 —— 見下方 instance 表。 | +| `instances` | `instances: HashMap` ✓ | 角色相同;欄位集合精簡。 | +| `teams` | `teams: HashMap` ✓ | Rust 形狀 (`src/fleet.rs:175-183`):`{ members, orchestrator?, description? }`。新增的 `orchestrator: Option` 欄位指定一個成員作為 team-addressed `delegate_task` 的 routing 目標,並用於 TUI team-tab 視圖的分組。TS `TeamConfig` 只有 `{ members, description? }` —— port 時若想保持 TS 等價行為,`orchestrator` 留空即可;採用新 routing 慣例後再設定。 | +| `templates` | `templates: Option>` | **Rust 目前僅 parser** —— 鍵會 round-trip,但 template 展開尚未實作。**不要** 依賴 TS template 語意。 | +| `profiles` | **移除** | Per-instance 欄位攤平;將來若需要,reusable profiles 會以另一個 concern 重新引入。 | +| `health_port` | **移除** | Daemon 不開獨立 health port;改用 `agend-terminal doctor`。 | +| `stt` | **移除** | Voice→cloud 轉錄未實作;不支援 voice 訊息。 | + +### `instances.` 欄位 diff + +`@suzuke/agend` `InstanceConfig` (`src/types.ts:65-111`) → `agend-terminal` `InstanceConfig` (`src/fleet.rs:140-183`): + +| TS 欄位 | Rust 欄位 (snake_case) | 遷移動作 | +|---|---|---| +| `working_directory` | `working_directory: Option` | ✓ 相同。實務上必填。 | +| `display_name` | `display_name: Option` | ✓ 相同。 | +| `description` | `role: Option` (帶 serde alias `description`) | **改名或用 alias。** Rust 上 `role: …` 與 `description: …` 都接受;canonical 是 `role`。 | +| `tags` | **移除** | 砍掉欄位。無替代;Rust 不採 tag-based routing。 | +| `topic_id` | `topic_id: Option` | ✓ 相同;**僅裸 int** (見 High-friction #2)。 | +| `general_topic` | **移除** | 砍掉欄位。 | +| `restart_policy` | **移除** | 砍掉欄位。Restart 邏輯 daemon-managed,固定常數在 `health.rs` (`CRASH_WINDOW=10min`、`BACKOFF_BASE=5s`、`BACKOFF_MAX=300s`、`DEFAULT_MAX_RETRIES=5`)。Per-instance restart 不開放調整。 | +| `context_guardian` | **移除** | 砍掉欄位。Context-rotation 調整改住 backend presets / runtime defaults。 | +| `log_level` | **移除** | 砍掉欄位。改用 env var (`RUST_LOG=…`) 全域設定。 | +| `backend` | `backend: Option` | ✓ 相同。Backend 識別字串應一致 (claude / gemini / codex / opencode / kiro)。 | +| `tool_set` | **移除** | 砍掉欄位。MCP tool ACL 改成啟動 daemon 時設 env var `AGEND_MCP_TOOLS_ALLOW` / `AGEND_MCP_TOOLS_DENY`。 | +| `lightweight` | **移除** | 砍掉欄位。 | +| `systemPrompt` | **移除** | 砍掉欄位。指令改透過 per-backend `instructions_path` 檔案 (例如 workspace `CLAUDE.md`) 注入 —— 路徑見 `agend-terminal` 對應的 backend preset。TS `file:`-chain 語法 Rust 上無等價;若需要請手動拼接。 | +| `skipPermissions` | **移除** | 砍掉欄位。Permission flags 烘進每個 backend preset 的 `spawn_flags()`。 | +| `model` | `model: Option` | ✓ 相同。傳成 `--model` 給 backend CLI。 | +| `model_failover` | **移除** | 砍掉欄位。Rust 目前無自動 failover;主 model rate-limit 時手動換 model 重啟。 | +| `cost_guard` | **移除** | 砍掉欄位。Rust 上目前無等價物。 | +| `worktree_source` | `git_branch: Option` (帶 serde alias `worktree_source`) | **改名或用 alias。** Rust canonical 是 `git_branch`;TS 拼寫透過 alias 仍會載入。 | +| `workflow` | **移除** | 砍掉欄位。 | +| `startup_timeout_ms` | **移除** | 砍掉欄位。Startup 計時由各 backend preset 的 `ready_timeout_secs` 控制。 | +| `agent_mode` | **移除** | 砍掉欄位。通訊模式 (MCP vs HTTP) 由 backend preset 決定;不開放 per-instance 調整。 | + +**Port 時可能想加的 Rust-only 欄位:** + +| Rust 欄位 | 型別 | 用途 | +|---|---|---| +| `receive_fleet_updates` | `Option` | 預設 opt-in。對不該收到 fleet `` 注入的 instance 設 `false`。 | +| `cols`、`rows` | `Option` | 覆寫該 instance 的 PTY 尺寸。 | +| `env` | `HashMap` | Per-instance 加 env。注意:Rust 會依 `agent.rs::SENSITIVE_ENV_KEYS` 過濾類 credential 鍵 —— 從這裡注入的 secrets 可能會被 redacted。 | +| `command`、`args`、`ready_pattern` | low-level overrides | 用於選的 backend 不是內建 preset 的情況 (legacy / 客製化 CLI)。 | + +**snake_case 注意:** TS schema 混用 `camelCase` (`systemPrompt`、`skipPermissions`) 與 `snake_case` (其餘)。Rust 一律 `snake_case`。上述兩個 TS camelCase 欄位 Rust 端都已移除,所以這個差異實際只影響 `worktree_source` / `git_branch` (alias 兩邊收) 與 `description` / `role` (alias 兩邊收)。 + +## Backend invocation diff {#backend-invocation-diff} + +*(Phase B —— 涵蓋:`--mcp-config` 對應到 Rust 等價物、`--append-system-prompt-file` 流程、per-backend env-var 注入、MCP server respawn 語意、fleet-instructions 傳遞通道 —— TS 端 post-#55 模型也可參考 `docs/fleet-instructions-injection.md`。)* + +## MCP tool API diff {#mcp-tool-api-diff} + +*(Phase B —— 涵蓋:MCP tools 完整清單 (TS full set 約 20 個)、name rename、argument 形狀變更、return 值差異、broadcast `cost_limited` 欄位接續、deferred 或移除的 tools。)* + +## Migration steps {#migration-steps} + +*(Phase C —— 涵蓋:pre-flight 檢查清單、`agend-terminal migrate` (若提供) 呼叫、雙跑期間、rollback、post-migration 驗證。)* + +## Known incompatibilities and deferred parity {#known-incompatibilities} + +*(Phase C —— 涵蓋:意圖不 port 的功能、被延後到後續 Rust release 的 parity 項目、仍在設計中的項目。)*