diff --git a/README.md b/README.md index 2b5974f..ced75a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # `fleetcom` -`fleetcom` supervises your local development fleet: multiplexers, application servers, REPLs, builds, tests, and AI agent sessions. +Use `fleetcom` to supervise your local development fleet: multiplexers, application servers, REPLs, builds, tests, and AI agent sessions. ## Key Features @@ -24,32 +24,32 @@ Press `Enter` to take control of a task, then `Ctrl-\` to return to the dashboar ### Custom groups -Organize related tasks into named groups, even when they run in different directories. +Organize related tasks into named groups across working directories. ![`fleetcom` custom-group view](docs/img/groups.png) ### Resume agent sessions -Start `claude`, `codex`, `grok`, or `omp` normally. When you rerun the task or reload a saved session, `fleetcom` resumes the conversation when its ID is known; otherwise, it runs the authored command. +Start `claude`, `codex`, `grok`, or `omp` normally. Rerun the task or reload a saved session to resume the conversation. ## Operational model -Running several long-lived commands is pesky once they span terminal panes or need to survive a disconnect. `fleetcom`: +Managing several long-lived commands across terminal panes is pesky, especially when you need to reconnect later. With `fleetcom`: -- Runs each command in its own PTY and groups tasks by state, working directory, or named group. -- Delegates tasks to a daemon, so a disconnecting client stops nothing. -- Saves and reloads task recipes: directories, commands, group assignments, and display names. -- Reruns a completed task in place, keeping its identity, group, and name. -- Captures `claude`, `codex`, `grok`, and `omp` conversation IDs for saved and rerun tasks. -- Automatically snapshots the current task set for recovery. +- Run each command in its own PTY and group tasks by state, working directory, or named group. +- Keep tasks running under a daemon across client disconnects. +- Save and reload task recipes: directories, commands, group assignments, and display names. +- Rerun a completed task in place, keeping its identity, group, and name. +- Save and rerun `claude`, `codex`, `grok`, and `omp` tasks with captured conversation IDs. +- Recover the current task set from automatic snapshots. ## Documentation -The [`docs/`](docs/README.md) directory covers configuration, on-disk state, session files, resuming supported agent sessions, commands, and a complete first run. +See [`docs/`](docs/README.md) for configuration, on-disk state, session files, resuming supported agent sessions, commands, and a complete first run. ## Installation -Unix only: it relies on PTYs and process-group signals (`killpg`). +Unix only: PTYs and process-group signals (`killpg`) are required. ### Cargo (recommended) @@ -59,7 +59,7 @@ For normal use, install the published crate from [crates.io](https://crates.io/c cargo install fleetcom ``` -[Source installation](docs/README.md#installation-from-source) covers builds from a repository clone. +See [Source installation](docs/README.md#installation-from-source) to build from a repository clone. ## Usage @@ -69,13 +69,13 @@ Connect to the daemon, autostarting it when necessary, and open the dashboard by fleetcom ``` -The [invocation reference](docs/commands.md#invocation) covers sessions, foreground mode, scrollback, and daemon shutdown. +See the [invocation reference](docs/commands.md#invocation) for sessions, foreground mode, scrollback, and daemon shutdown. ## Key Commands ### Dashboard -The dashboard shows two short key hints; `?` opens an expanded key reference: +Use the two dashboard hints for common actions; press `?` for the expanded key reference: ```text ❯ n run · @ dir · / find · s sort @@ -84,35 +84,35 @@ The dashboard shows two short key hints; `?` opens an expanded key reference: ### Attached -- `Ctrl-\` backgrounds the task and returns to the dashboard. +- Press `Ctrl-\` to background the task and return to the dashboard. - Other supported input is forwarded to the task's PTY. -[`docs/commands.md`](docs/commands.md#dashboard) covers every key and launch flag, including the routing mechanics. +See [`docs/commands.md`](docs/commands.md#dashboard) for every key and launch flag, including the routing mechanics. ## How it works -Every task runs in its own pseudo-terminal, emulated with `alacritty_terminal`. The dashboard preview, peek overlay, and attached view all read the same emulated screen grid, so full-screen programs such as `vim` and `htop` retain one consistent terminal state across views. [`docs/how-it-works.md`](docs/how-it-works.md) documents the terminal emulation, input routing, and activity grouping. +`fleetcom` runs each task in a separate pseudo-terminal, emulated with `alacritty_terminal`. The dashboard preview, peek overlay, and attached view all read the same emulated screen grid. This preserves terminal state as you move between views, including for full-screen programs such as `vim` and `htop`. See [`docs/how-it-works.md`](docs/how-it-works.md) for terminal emulation, input routing, and activity grouping. ## Scope and tradeoffs -`fleetcom` targets concurrent build, test, watch, server, and interactive-agent processes. Each task is one command rather than a persistent shell session. +Use `fleetcom` for concurrent build, test, watch, server, and interactive-agent processes. Each task is one command rather than a persistent shell session. ### When to use `fleetcom` -- Several long-lived commands need one place for observation, tagging, and attachment. -- Jobs must survive a terminal closing and remain available for reattachment. +- You need one place to observe, tag, and attach to several long-lived commands. +- You need to reattach to jobs after closing a terminal. - The same command set is launched often enough to justify a saved session. - Captured agent conversations (`claude`, `codex`, `grok`, `omp`) should resume on rerun. ### When to avoid `fleetcom` -- You primarily need persistent interactive shell workspaces; use `tmux` or `zellij` directly. `fleetcom` can supervise a multiplexer, but it does not replace one. +- You primarily need persistent interactive shell workspaces; use `tmux` or `zellij` directly. You can supervise a multiplexer with `fleetcom`, but cannot use it as a persistent shell workspace. - You need a full process manager: the fleet’s lifetime is bounded by the daemon’s. ### Operational limits -- The fleet dies with the daemon: the daemon process is the fleet's single point of failure. -- Commands run through the client's non-interactive shell (`$SHELL -c`, or `/bin/sh` when `SHELL` is unset), so functions and aliases defined in `~/.zshrc` are not available. -- The daemon serves one client at a time. +- The fleet's lifetime is bounded by the daemon's: the daemon process is the single point of failure. +- Commands are executed through the client's non-interactive shell (`$SHELL -c`, or `/bin/sh` when `SHELL` is unset), so functions and aliases defined in `~/.zshrc` are not available. +- Only one client can be connected to the daemon at a time. -[Operational constraints](docs/README.md#operational-constraints) documents the shutdown and signal mechanics. +See [Operational constraints](docs/README.md#operational-constraints) for shutdown and signal mechanics. diff --git a/docs/README.md b/docs/README.md index bae8e88..c4a6517 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,13 +1,13 @@ # fleetcom Documentation -`fleetcom` has two kinds of state with different lifetimes: the daemon owns live processes, while session files store repeatable launch recipes. Confusing those boundaries makes shutdown, reconnect, and session behavior difficult to reason about. This guide documents the paths, the lifecycle, and a complete first run. +The daemon supervises live processes; session files store repeatable launch recipes. Reconnecting returns you to the daemon's running tasks, while loading a session starts new processes from its recipe. Use this guide for storage paths, lifecycle details, and a complete first run. ## Index - [Commands](commands.md): every key and launch flag, including the routing mechanics - [How it works](how-it-works.md): the PTY emulation, input routing, and activity grouping -- [Sessions](sessions.md): the task recipe format and where it lives -- [Agent session resume](agent-resume.md): how `fleetcom` captures and resumes supported `claude`, `codex`, `grok`, and `omp` sessions +- [Sessions](sessions.md): the task recipe format and storage location +- [Agent session resume](agent-resume.md): capturing and resuming supported `claude`, `codex`, `grok`, and `omp` sessions - [Storage paths](#storage-paths): runtime and session paths - [First-run walkthrough](#first-run-walkthrough): a first run, start to finish - [Security](#security): the trust boundary, on-disk state, and what is not protected @@ -15,21 +15,21 @@ ## Installation from source -`fleetcom` is Unix-only: it relies on PTYs and process-group signals (`killpg`). +`fleetcom` is Unix-only: PTYs and process-group signals (`killpg`) are required. From a repository clone: -- `cargo test`: confirm the suite passes +- `cargo test`: run the test suite - `cargo build --release`: compile to `target/release/fleetcom` - `cargo install --path .`: put `fleetcom` on `PATH` ## Storage paths -Runtime state contains the daemon socket, lock, and log. Configuration contains durable session recipes. The paths resolve independently. +The daemon socket, lock, and log are stored under the runtime path; durable session recipes are stored under the configuration path. These paths are resolved independently. ### Runtime directory (socket + lock + log) -The runtime directory holds `default.sock`, the client↔daemon socket; `daemon.lock`, the single-instance `flock`; and `daemon.log`, the stderr of an autostarted daemon. The daemon records its PID in the lock file; `--kill` uses that PID rather than waiting for the socket. [Security](#security) documents the permissions and the ownership checks this directory must satisfy. +The runtime directory contains three files: `default.sock` carries client↔daemon traffic, `daemon.lock` holds the single-instance `flock`, and `daemon.log` records an autostarted daemon's stderr. The daemon writes its PID to the lock file so `--kill` can signal it without waiting for the socket. See [Security](#security) for the required permissions and ownership checks. Resolved in this order: @@ -39,11 +39,11 @@ Resolved in this order: | 2 | `$XDG_RUNTIME_DIR` is set and non-empty | `$XDG_RUNTIME_DIR/fleetcom` | | 3 | otherwise | `$TMPDIR/fleetcom-$uid` | -`$XDG_RUNTIME_DIR` is honored on every supported platform. On macOS, `$TMPDIR` is already per-user. The `$uid` suffix also separates users when the fallback resolves beneath a shared `/tmp`. +`$XDG_RUNTIME_DIR` is honored on every supported platform. On macOS, `$TMPDIR` is already per-user. With the `$uid` suffix, users are also separated when the fallback path is beneath a shared `/tmp`. ### Config directory (sessions) -Holds saved sessions under a `sessions/` subdirectory: one sanitized-name `.json` file per session. See [Sessions](sessions.md) for the format. +Saved sessions are stored under a `sessions/` subdirectory: one sanitized-name `.json` file per session. See [Sessions](sessions.md) for the format. | Order | Condition | Path | | -- | -- | -- | @@ -51,13 +51,13 @@ Holds saved sessions under a `sessions/` subdirectory: one sanitized-name `.json | 2 | Linux | `${XDG_CONFIG_HOME:-~/.config}/fleetcom/sessions` | | 2 | macOS | `~/Library/Application Support/fleetcom/sessions` | -The platform default is [`dirs::config_dir()`](https://docs.rs/dirs/latest/dirs/fn.config_dir.html) joined with `fleetcom`. The first save creates any missing session directories. +The platform default is [`dirs::config_dir()`](https://docs.rs/dirs/latest/dirs/fn.config_dir.html) joined with `fleetcom`. Missing session directories are created on the first save. ## First-run walkthrough -The following walkthrough moves from an empty dashboard to a saved fleet. The frames show layout, not captured terminal output. +Follow these steps from an empty dashboard to a saved fleet. The frames below are layout illustrations, not captured terminal output. -Run `fleetcom`. The first invocation starts the daemon and opens an empty dashboard: +Run `fleetcom` to start the daemon and open an empty dashboard: ```text fleetcom 0 running · 0 idle · 0 done by state · dir · custom @@ -66,7 +66,7 @@ Run `fleetcom`. The first invocation starts the daemon and opens an empty dashbo ↑↓ select · enter attach · space peek · ? controls ``` -The hint rows cover common dashboard actions; `?` opens an expanded key reference. Press `n`, enter a command, and press `Enter`. The command runs in its own PTY and appears under Running. Repeat the process for a second command: +Use the hints for common dashboard actions; press `?` for the expanded key reference. Press `n`, enter a command, and press `Enter`. The command is started in its own PTY and listed under Running. Repeat the process for a second command: ```text fleetcom 2 running · 0 idle · 0 done by state · dir · custom @@ -79,7 +79,7 @@ The hint rows cover common dashboard actions; `?` opens an expanded key referenc ↑↓ select · enter attach · space peek · ? controls ``` -Each row is `glyph · tag · command · latest output · age`. The age counts from the task's last meaningful edge: launch while running, last output once idle, exit once completed. `Space` peeks: a read-only box of the selected task's live screen, without leaving the dashboard: +Each row is `glyph · tag · command · latest output · age`. Age is measured from launch while running, last output once idle, or exit once completed. Press `Space` to peek at the selected task's live screen in a read-only box over the dashboard: ```text ┌─ cargo watch -x test ───────────────────────────────┐ @@ -89,13 +89,13 @@ Each row is `glyph · tag · command · latest output · age`. The age counts fr └ space/esc close · enter attach · preview: floor ────┘ ``` -`Enter` attaches to the task. Keystrokes then go to its PTY, except for the reserved background chord shown in the status bar: +Press `Enter` to attach to the task. Keystrokes are then forwarded to its PTY, except for the reserved background chord shown in the status bar: ```text [attached] npm run dev Ctrl-\ background ``` -`Ctrl-\` returns to the dashboard. `m` tags the selected task "in use," adding `◆` and moving it to the first section: +Press `Ctrl-\` to return to the dashboard. Press `m` to tag the selected task "in use," mark it `◆`, and place it in the first section: ```text fleetcom 2 running · 0 idle · 0 done by state · dir · custom @@ -107,7 +107,7 @@ Each row is `glyph · tag · command · latest output · age`. The age counts fr ✻ npm run dev VITE v5.0 ready in 312 ms 1m ``` -`s` cycles through state, directory, and custom grouping. The header renders the active mode in bold. In custom mode, `g` assigns the selected task to a named group. Named sections sort without regard to case, so `API` and `api` are adjacent. They remain separate because group identity is case-sensitive. Unassigned appears last when at least one task has no group: +Press `s` to cycle through state, directory, and custom grouping. The active mode is bold in the header. In custom mode, press `g` to assign the selected task to a named group. Named sections are sorted without regard to case, so `API` and `api` are adjacent. They remain separate because group identity is case-sensitive. Unassigned is listed last when at least one task has no group: ```text fleetcom 2 running · 0 idle · 0 done by state · dir · custom @@ -119,9 +119,9 @@ Each row is `glyph · tag · command · latest output · age`. The age counts fr ✻ npm run dev VITE v5.0 ready in 312 ms 2m ``` -In custom mode, a new command inherits the selected task's group. The spawn prompt makes that destination explicit (`❯ api ▸ cargo run`). Group assignments belong to task state, so detach and rerun preserve them. The [command reference](commands.md#the-g-group-picker) documents the picker mechanics. +In custom mode, a new command is assigned the selected task's group, shown in the spawn prompt (`❯ api ▸ cargo run`). Group assignments are stored with task state and preserved across detach and rerun. See the [command reference](commands.md#the-g-group-picker) for picker mechanics. -`R` renames the selected task. The prompt opens with the current name; `Enter` saves, an empty field restores the command as the display label, and `Esc` cancels. Rows and peek titles then show the name in place of the command: +Press `R` to rename the selected task, starting with its current name. Press `Enter` to save, or `Esc` to cancel. Leave the field empty to display the command again. After naming a task, its name is displayed in dashboard rows and peek titles: ```text fleetcom 2 running · 0 idle · 0 done by state · dir · custom @@ -133,15 +133,15 @@ In custom mode, a new command inherits the selected task's group. The spawn prom ✻ npm run dev VITE v5.0 ready in 312 ms 3m ``` -The attached status bar shows both: `[attached] api tests · cargo watch -x test`. Names are daemon state, survive detach and rerun, and persist in saved [sessions](sessions.md). +In the attached status bar, both are displayed: `[attached] api tests · cargo watch -x test`. Names are stored in daemon state, preserved across detach and rerun, and included in saved [sessions](sessions.md). -`w`, a name, and `Enter` save the fleet as a [session](sessions.md). `q` then disconnects while the daemon and both tasks continue running. A subsequent `fleetcom` invocation reconstructs the dashboard from the daemon's current task state. `Q` or `fleetcom --kill` stops the tasks (`TERM`, then `KILL` after a two-second grace period) and exits the daemon. +Press `w`, type a name, and press `Enter` to save the fleet as a [session](sessions.md). Press `q` to disconnect while the daemon and both tasks continue running. Run `fleetcom` again to view the daemon's current task state. Press `Q` or run `fleetcom --kill` to stop the tasks (`TERM`, then `KILL` after a two-second grace period) and exit the daemon. ## Security -`fleetcom` runs entirely as your user. It neither raises nor drops privileges. Access control comes from filesystem permissions rather than authentication: the socket is mode `0600` inside a mode-`0700` directory, and the daemon performs no peer check. Any process running as your user can therefore connect, spawn commands, and read task output. That is the trust boundary. +`fleetcom` runs entirely as your user, without raising or dropping privileges. Filesystem permissions control access: the socket is mode `0600` inside a mode-`0700` directory, and the daemon performs no peer authentication. Any process running as your user can therefore connect, spawn commands, and read task output. That is the trust boundary. -### What lands on disk +### On-disk state | Path | Mode | Contents | | -- | -- | -- | @@ -155,43 +155,43 @@ The attached status bar shows both: `[attached] api tests · cargo watch -x test | `/recovery/.json` | `0600` | one automatic session recipe | | `/-/` | `0700` | [agent hook, notifier, and extension-module assets plus per-run capture payloads](agent-resume.md#capture-state-and-isolation) | -Saves are atomic: `fleetcom` writes a mode-`0600` temporary file in the destination directory, syncs it, then renames it over the target. This does not expose a partial or world-readable recipe. New session and recovery directories use mode `0700`; each save also removes group and other permissions from the destination directory. +Saves are atomic: a mode-`0600` temporary file is written in the destination directory, synced, then renamed over the target. No partial or world-readable recipe is exposed. New session and recovery directories are created with mode `0700`; group and other permissions are removed from the destination directory on each save. ### The runtime directory must be trustworthy -`fleetcom` validates the runtime directory before trusting its contents. The path must be a real directory owned by the current user; symlinks and directories owned by another user are rejected. Group or other write access is fatal because another user could already have planted entries. Any remaining group or other permissions are removed in place. +The runtime directory is validated before its contents are used. The path must be a real directory owned by the current user; symlinks and directories owned by another user are rejected. Group or other write access is fatal because another user could already have planted entries. Any remaining group or other permissions are removed in place. ### What is not protected -Recipes persist full command lines, which can embed secrets. A token passed as an argument is written to its session file and to every recovery snapshot that captures the task. +Full command lines are persisted in recipes, including any embedded secrets. A token passed as an argument is written to its session file and to every recovery snapshot that captures the task. -`fleetcom` does not persist the client environment. Each client sends its environment and working directory during the connection handshake, and the daemon retains that launch context in memory. Session and recovery files store only directories, commands, group assignments, and display names. +The client environment is not persisted. Each client's environment and working directory are sent during the connection handshake and retained in daemon memory. Only directories, commands, group assignments, and display names are stored in session and recovery files. -### Captured IDs cross a shell boundary +### Captured IDs in shell commands -Agent resume writes a captured conversation ID into a command run through `$SHELL -c`, so validation is a security boundary. Accepted IDs contain only lowercase hexadecimal in the `8-4-4-4-12` UUID shape. Capture payloads, live session records, and the command builder all apply that check. Terminal output supplies no session IDs. Instrumentation applies only to a bare program word or its canonical resume form, never arbitrary shell text. [Agent session resume](agent-resume.md#validation-boundary) documents both boundaries. +To resume an agent conversation, `fleetcom` inserts its captured ID into a command run through `$SHELL -c`. Because the ID becomes shell input, validation accepts only lowercase hexadecimal in the `8-4-4-4-12` UUID shape. The same check applies to capture payloads, live session records, and final command construction. `fleetcom` does not read session IDs from terminal output and instruments only a bare program word or its canonical resume form. See [Agent session resume](agent-resume.md#validation-boundary) for both boundaries. -### Copied text leaves through the terminal +### Copying text through the terminal -When `fleetcom` copies a selection or forwards an attached task's clipboard store, it sends the text to the host terminal as an OSC 52 escape sequence. The sequence also crosses intermediaries such as SSH connections and terminal multiplexers. +When you copy a selection or forward an attached task's clipboard store, the text is sent to the host terminal as an OSC 52 escape sequence, including through intermediaries such as SSH connections and terminal multiplexers. ## Operational constraints -### The fleet dies with the daemon +### Task lifetime after daemon termination -Because the daemon holds each PTY master, daemon termination closes the terminals and the kernel sends `SIGHUP` to every task's process group. A clean shutdown sends `SIGTERM` before `SIGKILL`; a crash or direct `SIGKILL` provides no grace period. HUP-immune processes (`nohup`, `trap '' HUP`) can survive, but the next daemon neither owns nor displays them. A panic while serving one client only drops that connection. +Each PTY master is held by the daemon. On daemon termination, the terminals are closed and `SIGHUP` is sent by the kernel to every task's process group. On clean shutdown, `SIGTERM` is sent before `SIGKILL`; after a crash or direct `SIGKILL`, no grace period is available. HUP-immune processes (`nohup`, `trap '' HUP`) can be left running, but are neither supervised nor displayed by the next daemon. After a panic while serving one client, only that connection is dropped. ### Commands run through the client's non-interactive shell -(`$SHELL -c`, or `/bin/sh` when `SHELL` is unset), so functions and aliases from `~/.zshrc` are unavailable. +`fleetcom` invokes `$SHELL -c`, falling back to `/bin/sh` when `SHELL` is unset. Because the shell is non-interactive, functions and aliases from `~/.zshrc` are unavailable. ### Environment and directory -Each client sends its environment and working directory once during the connection handshake. Spawns, reruns, and session loads initiated by that client use the same launch context. [Security](#security) covers what persists. +Each client's environment and working directory are sent once during the connection handshake. This launch context is used for subsequent spawns, reruns, and session loads from that client. See [Security](#security) for persisted state. ### Scrollback depth is fixed per supervisor -Each task's terminal keeps a scrollback history whose depth is resolved once, when the owning supervisor starts: +Per-task scrollback depth is resolved once, at supervisor startup: | Order | Condition | Depth | | -- | -- | -- | @@ -199,24 +199,24 @@ Each task's terminal keeps a scrollback history whose depth is resolved once, wh | 2 | `FLEETCOM_SCROLLBACK` parses as a whole number | that value, clamped to 100,000 | | 3 | otherwise | 2,000 | -`0` disables scrollback. An unparseable `FLEETCOM_SCROLLBACK` falls back to 2,000 rather than failing daemon startup. The daemon resolves the depth at startup from its inherited environment, so a changed value reaches only the tasks of a new daemon; stop the current one with `fleetcom --kill` first. `--foreground` resolves the depth in-process for each invocation. +Set `0` to disable scrollback. An unparseable `FLEETCOM_SCROLLBACK` is treated as 2,000 without failing daemon startup. Depth is resolved at daemon startup from the inherited environment. To change it, stop the current daemon with `fleetcom --kill` first. With `--foreground`, depth is resolved in-process for each invocation. ### Client and daemon protocol versions must match -The daemon rejects a mismatch during the handshake. Stop an incompatible daemon with `fleetcom --kill`, which also terminates every running task, then start a new client. +Mismatched versions are rejected during the handshake. Stop an incompatible daemon with `fleetcom --kill`, which also terminates every running task, then start a new client. ### The daemon serves one client at a time -A second `fleetcom` prints a waiting notice, then attaches when the active client disconnects (`q`). `Ctrl-C` while waiting aborts without touching the daemon. +When you run a second `fleetcom`, a waiting notice is printed. You can attach once the active client disconnects (`q`). Press `Ctrl-C` while waiting to abort without affecting the daemon. ### Shutdown is graceful-first -`X`, `Q`, `--kill`, and daemon shutdown signals send `SIGTERM` to each task's *process group*, then escalate to `SIGKILL` after two seconds. Exited leaders remain unreaped through escalation, reserving the process-group IDs so background children remain signalable. Full shutdown (`Q`/`--kill`) waits one shared grace period even when all listed tasks have finished or exit on `TERM`; with no tasks left to clean up, shutdown returns immediately. Tasks already terminating keep their original escalation timers. A child created by `cmd &` in a non-interactive shell normally remains in its parent's group. A process that calls `setsid` or otherwise leaves the group is outside the sweep and must be terminated separately. +On `X`, `Q`, `--kill`, or a daemon shutdown signal, `SIGTERM` is sent to each task's *process group*, then `SIGKILL` after two seconds. Exited leaders remain unreaped through escalation, reserving the process-group IDs so background children remain signalable. On full shutdown (`Q`/`--kill`), one shared grace period is required even when all listed tasks have finished or exited on `TERM`; with no tasks left to clean up, shutdown is immediate. Original escalation timers are retained for tasks already terminating. A child created by `cmd &` in a non-interactive shell normally remains in its parent's group. A process that calls `setsid` or otherwise leaves the group is outside the sweep and must be terminated separately. ### `--foreground` is ephemeral -It runs the core in-process with no daemon, so the tasks die when you quit and there is nothing to reattach to. +Run the core in-process with no daemon. Tasks are terminated when you quit; you cannot reattach later. ### Signalling the daemon is a clean shutdown -`SIGTERM`/`SIGINT`/`SIGHUP` to the daemon group-kill every task, remove the socket, and exit. This is the same teardown as `Q` or `fleetcom --kill`. +Send `SIGTERM`, `SIGINT`, or `SIGHUP` to the daemon to group-kill every task, remove the socket, and exit. This is the same teardown as `Q` or `fleetcom --kill`. diff --git a/docs/agent-resume.md b/docs/agent-resume.md index eb13a89..626fc8b 100644 --- a/docs/agent-resume.md +++ b/docs/agent-resume.md @@ -1,23 +1,23 @@ # Agent session resume -Session files preserve launch commands, not process state. Relaunching a bare `claude`, `codex`, `grok`, or `omp` command ordinarily starts another conversation. For accepted commands, `fleetcom` captures a validated conversation ID when available and builds a canonical resume command when saving a session or rerunning a finished task (`r`). +Session files store launch commands, not process state. If you relaunch a bare `claude`, `codex`, `grok`, or `omp` command, you ordinarily start another conversation. To preserve that conversation, `fleetcom` captures a validated ID when one is available for an accepted command. It uses that ID to construct a canonical resume command when you save a session or rerun a finished task (`r`). ## Workflow Start a supported agent without flags: -1. Press `n` and run `claude`, `codex`, `grok`, or `omp`. The task appears in the dashboard under the command you typed. Instrumentation changes only the string executed through `$SHELL -c`, so a direct spawn still displays the requested command. -2. Work in it. `Enter` attaches; `Ctrl-\` returns to the dashboard. Depending on the agent, `fleetcom` pins an ID at launch and may update it from a hook, notifier, extension, or matching live registry record. -3. Press `w`, enter a session name, and press `Enter`. A captured bare command becomes its canonical resume form, such as `claude --resume ''`. Without a known ID, the save preserves the authored command. -4. Run `fleetcom `, or press `o` in the dashboard, to start new processes from the saved commands. A stored resume command reopens its captured conversation. +1. Press `n` and run `claude`, `codex`, `grok`, or `omp`. The task is listed under the command you typed. Only the string executed through `$SHELL -c` is instrumented; the requested command is still displayed for a direct spawn. +2. Work in it. Press `Enter` to attach; press `Ctrl-\` to return to the dashboard. Depending on the agent, an ID is pinned at launch and may be updated from a hook, notifier, extension, or matching live registry record. +3. Press `w`, enter a session name, and press `Enter`. A captured bare command is saved in canonical resume form, such as `claude --resume ''`. Without a known ID, the authored command is preserved. +4. Run `fleetcom `, or press `o` in the dashboard, to start new processes from the saved commands. Use a stored resume command to reopen the captured conversation. -On a finished agent task, `r` uses the captured launch, hook, notifier, extension, or registry ID. A registry record remains eligible after exit if it is still present. The replacement keeps the task's ID, tag, group, and name. After a successful rewrite, the row shows the resume command because it has become the task's launch recipe; a [saved session](sessions.md) records the same string. +Press `r` on a finished agent task to rerun it with the captured launch, hook, notifier, extension, or registry ID. A registry record is still eligible after exit if present. The task's ID, tag, group, and name are preserved. After a successful rewrite, the resume command is displayed in the row and stored as the launch recipe. The same string is written to a [saved session](sessions.md). -Capture is best-effort and narrow by design. A command carrying a prompt, extra flags, or shell syntax stays opaque and saves verbatim. An accepted command with no available ID also saves unchanged. In both cases, loading the recipe reruns the original command. +Capture is best-effort and narrow by design. Commands with prompts, extra flags, or shell syntax are treated as opaque and saved verbatim. Accepted commands with no available ID are also saved unchanged. In both cases, the original command is rerun on load. ## Accepted command boundary -The capture boundary is intentionally narrow. Only these forms participate: +The capture boundary is intentionally narrow. Only these forms are accepted: - `claude`, `codex`, `grok`, or `omp` - `claude --resume ` @@ -27,80 +27,80 @@ The capture boundary is intentionally narrow. Only these forms participate: The program word may be a path such as `/usr/local/bin/claude` when its basename matches and the token contains no shell syntax. A resume UUID may be bare or single-quoted, but it must be the final argument. -Everything else remains opaque and runs, displays, and saves verbatim. This includes prompts, flags, alternate resume spellings, subcommands, trailing arguments, and shell syntax. The narrow boundary prevents injected arguments from binding to a different shell command than the detector recognized. +Everything else is treated as opaque and executed, displayed, and saved verbatim. This includes prompts, flags, alternate resume spellings, subcommands, trailing arguments, and shell syntax. With this boundary, injected arguments cannot be bound to a different shell command than the one recognized during detection. ## Capture state and isolation -Hooks, notifiers, and extension modules are loaded by the agent rather than the supervisor, so they need stable paths. The supervisor installs those assets once for each runtime root. An explicit `FLEETCOM_RUNTIME_DIR` becomes that root. Otherwise, `fleetcom` uses the platform runtime or cache directory and partitions it by session directory. +Hooks, notifiers, and extension modules are loaded by the agent rather than the supervisor, so they need stable paths. Assets are installed once per runtime root. An explicit `FLEETCOM_RUNTIME_DIR` is used as the root; otherwise, the platform runtime or cache directory is used, partitioned by session directory. -Each supervisor installation creates a private mode-`0700` `/-` namespace containing: +For each supervisor installation, a private mode-`0700` `/-` namespace is created with: - `claude-settings.json`, mode `0600` - `codex-notify.sh`, mode `0700` -- `omp-capture.js`, mode `0600`: omp imports the module rather than executing it, so it needs no executable bit +- `omp-capture.js`, mode `0600`: imported as a module, so no executable bit is required - `task--.json` capture paths -The random nonce separates concurrent supervisors and prevents PID reuse from selecting an existing namespace. The run number gives each rerun a distinct capture file, so a displaced process cannot overwrite the replacement run's session state. Installation leaves every other root entry unchanged. +The random nonce isolates concurrent supervisors and prevents PID reuse from selecting an existing namespace. Each rerun also gets a distinct run number, so a displaced process cannot overwrite the replacement run's capture file. Every other root entry is left unchanged during installation. ## Evidence sources ### `claude` -A bare Claude command can accept an ID at launch. `fleetcom` therefore generates a v4 UUID and adds the settings overlay: +Claude accepts an ID at launch. For a bare command, the harness therefore generates a v4 UUID and adds it with the settings overlay: ```text --session-id '' --settings '/claude-settings.json' ``` -A canonical resume command already supplies its conversation ID, so adding a second ID would be incorrect; it receives only `--settings`. The overlay installs a `SessionStart` hook that copies its JSON payload into `FLEETCOM_CAPTURE_FILE`, from which the harness reads `session_id`. +Since a canonical resume command already specifies the conversation ID, the harness adds only `--settings`. The overlay installs a `SessionStart` hook that copies its JSON payload into `FLEETCOM_CAPTURE_FILE`. The harness then reads `session_id` from that payload. -Claude also publishes one `/sessions/.json` record per session. `fleetcom` reads the direct path for the task leader's PID. When `$SHELL -c` leaves the shell as the task leader instead of replacing it with Claude, no matching record exists and the registry contributes no ID. +Claude session records are also available at `/sessions/.json`, one per session. The direct path for the task leader's PID is read. When the shell is retained as task leader under `$SHELL -c` instead of being replaced with Claude, no matching record is available and no ID is read from the registry. -A record counts only when its `kind` is `interactive` and its `pid`, `cwd`, and `startedAt` match the task. The PID must match the filename, the working directories must be identical or resolve to the same path, and the process start must fall within 30 seconds of the task spawn. Missing, malformed, or mismatched records contribute no evidence. The dashboard also maps a matching record's `waiting` status to the top tier of its [preview cascade](commands.md#peek); other statuses do not affect the preview. +A record is accepted only when its `kind` is `interactive` and its `pid`, `cwd`, and `startedAt` match the task. The PID must match the filename, the working directories must be identical or resolve to the same path, and the process start must fall within 30 seconds of the task spawn. Missing, malformed, or mismatched records are ignored. A matching record's `waiting` status is also mapped to the top tier of the [preview cascade](commands.md#peek); the preview is unchanged for other statuses. ### `codex` -Codex does not let the caller choose an ID at launch. Both accepted forms instead receive a notify override: +You cannot choose a Codex ID at launch. A notify override is injected into both accepted forms: ```text -c 'notify=["/codex-notify.sh"]' ``` -After each turn, the notifier writes the `agent-turn-complete` JSON argument to `FLEETCOM_CAPTURE_FILE`; the harness reads `thread-id`. This captures in-TUI session changes after the resumed conversation completes a turn. +After each turn, the notifier writes the `agent-turn-complete` JSON argument to `FLEETCOM_CAPTURE_FILE`, and the harness reads `thread-id` from it. This captures an in-TUI session change after a turn completes in the resumed conversation. -Replacing a configured notifier would change user behavior. `fleetcom` reads bare top-level keys in `$CODEX_HOME/config.toml` until the first table header. A one-line `notify` array of non-empty basic strings is chained after the capture write. Its argv is carried in `FLEETCOM_NOTIFY_CHAIN`, joined by newlines, and the notification payload is appended. An absent setting or empty array lets capture run alone; empty arguments, newlines, and NUL cannot be transported and disable injection. +Replacing a configured notifier would change user behavior, so the harness reads bare top-level keys in `$CODEX_HOME/config.toml` until the first table header. If it finds a one-line `notify` array of non-empty basic strings, it chains that notifier after the capture write. `FLEETCOM_NOTIFY_CHAIN` carries its argv joined by newlines; the capture script appends the notification payload before invoking it. An absent setting or empty array means capture runs alone. This encoding cannot transport empty arguments, newlines, or NUL, so those values disable injection. ### `grok` -Grok accepts a launch-time ID but exposes no injectable live-capture channel. A bare command therefore receives `--session-id ''`, while a canonical resume command needs no instrumentation. +You can specify a Grok ID at launch, but cannot inject a live-capture channel. For a bare command, `--session-id ''` is added; no instrumentation is required for a canonical resume command. ### `omp` -omp cannot pin an ID at launch: it has no `--session-id`, and `--resume` requires an existing session. Both accepted forms therefore receive the same injection and no pinned ID: +You cannot pin an omp ID at launch: no `--session-id` flag is available, and an existing session is required for `--resume`. The same injection is therefore added to both accepted forms, without a pinned ID: ```text -e '/omp-capture.js' ``` -`-e` loads the JavaScript module into the agent process and appends it to the user's extensions. Its `session_start` and `session_switch` handlers write `sessionId` as JSON to `FLEETCOM_CAPTURE_FILE`. The second handler follows in-TUI `/resume` changes. Capture writes are best-effort: the module returns when the capture path is empty and ignores write errors. +The `-e` flag loads the JavaScript module into the agent process, appending it to the user's extensions. Its `session_start` and `session_switch` handlers write `sessionId` as JSON to `FLEETCOM_CAPTURE_FILE`, including after in-TUI `/resume` changes. Capture writes are best-effort: the handlers return immediately for an empty capture path and ignore write errors. -The aliases `-r`, `--session`, and `-c` remain opaque because `fleetcom` rewrites only the canonical form it detects exactly. +The aliases `-r`, `--session`, and `-c` remain opaque because only the exactly detected canonical form is rewritten. ## ID precedence -Several channels can identify different conversations during one task. To make the result deterministic, `fleetcom` chooses the first available ID in this order: +Different conversation IDs may be available from different channels during one task. The first available ID is selected in this order: 1. The current capture-file payload. 2. The live session registry, implemented by `claude`. 3. The ID pinned or targeted at spawn. -Named saves, recovery snapshots, and reruns use this same precedence. `fleetcom` does not scan session stores to infer conversation ownership: a nearby transcript or rollout cannot identify which task owns it. +The same precedence is used for named saves, recovery snapshots, and reruns. Session stores are not scanned to infer conversation ownership: you cannot determine the owning task from a nearby transcript or rollout. -Terminal output never supplies a session ID: examples, quoted commands, and tool output can contain another conversation's valid UUID. An ID available only in an exit hint is not recovered. Without a capture, registry, or launch ID, the authored command remains unchanged. +Session IDs are never read from terminal output: another conversation's valid UUID may be present in examples, quoted commands, or tool output. An ID available only in an exit hint is not recovered. Without a capture, registry, or launch ID, the authored command remains unchanged. -The registry outranks the spawn pin because it can contain a session ID selected after launch, including one created by `/clear`. The capture file outranks the registry. +A registry ID is preferred over the spawn pin because it may have been selected after launch, including through `/clear`. A capture-file ID is preferred over the registry ID. -Saving and rerunning rewrite accepted commands to one of these forms: +On save and rerun, accepted commands are rewritten to one of these forms: ```text claude --resume '' @@ -109,24 +109,24 @@ grok --resume '' omp --resume '' ``` -The program word is preserved as typed. If no valid ID is available, the original command remains unchanged. A rerun increments the run number before spawning its replacement, so capture data from the displaced run cannot affect the new run. +The program word is preserved as typed. If no valid ID is available, the original command remains unchanged. On rerun, the run number is incremented before the replacement is spawned, isolating it from capture data for the displaced run. ## Validation boundary -Every captured value eventually enters a shell command, which makes validation the security boundary. Accepted IDs contain exactly lowercase hexadecimal characters in the `8-4-4-4-12` UUID shape. Capture payloads, registry records, and the final command builder all apply the same check. Malformed values are ignored rather than interpolated. A valid UUID alone does not establish conversation ownership. +Every captured value eventually enters a shell command, so validation accepts only lowercase hexadecimal characters in the `8-4-4-4-12` UUID shape. The same check applies to capture payloads, registry records, and final command construction. Malformed values are ignored rather than interpolated. A valid UUID alone does not establish conversation ownership. ## Extending capture -Each tool implements the `Harness` trait in [`src/harness/mod.rs`](../src/harness/mod.rs). The methods keep detection, evidence collection, and command construction separate: +Implement the `Harness` trait in [`src/harness/mod.rs`](../src/harness/mod.rs) for each tool. Separate detection, evidence collection, and command construction through these methods: -- `shape` supplies the program word and resume selector. The default `detect` and `resume_command` methods derive the accepted and canonical forms from that pair. -- `instrument` returns spawn-time arguments, environment entries, and an optional pinned ID. -- `parse_capture` reads an ID from hook, notify, or extension JSON. -- `live_session_id` reads the ID a live session publishes on disk. It defaults to `None` for tools that publish no registry. -- `live_blocked_status` reads that same registry for one display fact: whether the tool says it is blocked on the user. It returns preview text, never an ID, and defaults to `None`. -- `resolve_home` resolves configuration needed by instrumentation or the live registry. It defaults to `None`. +- `shape`: supply the program word and resume selector. Accepted and canonical forms are derived from that pair by the default `detect` and `resume_command` implementations. +- `instrument`: return spawn-time arguments, environment entries, and an optional pinned ID. +- `parse_capture`: read an ID from hook, notify, or extension JSON. +- `live_session_id`: read the ID published on disk for a live session. Return `None` by default when no registry is available. +- `live_blocked_status`: read that registry for blocked-on-user status. Return preview text, never an ID; return `None` by default. +- `resolve_home`: resolve configuration needed by instrumentation or the live registry. Return `None` by default. -The supervisor supplies the launch environment to `resolve_home`. Claude and Codex read their explicit override first, then `$HOME` plus their dot directory. When neither is supplied, configuration reads fall back to the supervisor's platform home. The resolved path stays attached to the task so Claude registry reads continue using its launch-time home after a reconnect. Grok and omp need no home resolution; their environment passes through to the child unchanged. +The launch environment is passed to `resolve_home` by the supervisor. For Claude and Codex, the explicit override is read first, then `$HOME` plus the tool's dot directory. With neither supplied, the supervisor's platform home is used. The resolved path is stored with the task and used for Claude registry reads after reconnect, preserving the launch-time home. For Grok and omp, no home resolution is required; the environment is passed to the child unchanged. ## Environment variables diff --git a/docs/commands.md b/docs/commands.md index 69faa03..e58bbd1 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,6 +1,6 @@ # Commands -`fleetcom` has two control surfaces. Launch arguments select the operating mode; keys control the dashboard, pickers, and attached PTY. `q`, `Q`, and `Ctrl-C` mean different things on each surface. +Choose the operating mode with launch arguments. Use keys to control the dashboard, pickers, and attached PTY. `q`, `Q`, and `Ctrl-C` mean different things on each surface. ## Invocation @@ -8,13 +8,13 @@ | -- | -- | | `fleetcom` | Connect to the daemon (autostarting it if needed) and open the dashboard | | `fleetcom ` | Load a saved [session](sessions.md) at startup, then open the dashboard | -| `fleetcom --foreground` | Run the core in-process, no daemon; tasks die when you quit | +| `fleetcom --foreground` | Run the core in-process, no daemon; tasks are terminated when you quit | | `fleetcom --scrollback ` | Set per-task scrollback depth (default 2,000, max 100,000); see [Scrollback](#scrollback) | -| `fleetcom --kill` | Stop the daemon and kill every task it owns; works even while another client is attached (it signals the daemon rather than queueing behind the socket) | +| `fleetcom --kill` | Stop the daemon and kill every task it owns; available even while another client is attached (shutdown is requested by signal, without waiting for the socket) | | `fleetcom --help` / `-h` | Print usage and exit | | `fleetcom --version` / `-V` | Print the version and exit | -`--daemon` is internal. An ordinary invocation starts it when necessary. The first non-`-` argument is treated as the session name; a second is rejected. +`--daemon` is internal. The daemon is started automatically when needed. The first non-`-` argument is treated as the session name; a second is rejected. ## Dashboard @@ -26,16 +26,16 @@ | `Space` | Peek at the selected task | | `n` | New command in the invocation directory | | `@` | New command in a directory you pick | -| `/` | Jump the selection to a task by name, command, or group (opens the [find palette](#the--find-palette)) | +| `/` | Jump the selection to a task by name, command, or group (through the [find palette](#the--find-palette)) | | `s` | Cycle grouping: by state / by directory / by custom group | | `m` | Tag the selected task "in use" (toggles) | | `M` | Select the next tagged task in dashboard order, wrapping at the end | -| `g` | Assign the selected task to a group (opens the group picker) | +| `g` | Assign the selected task to a group (through the group picker) | | `R` | Rename the selected task: a display name shown in place of the command | -| `r` | Rerun a finished task; supported agent tasks use the captured resume command | +| `r` | Rerun a finished task; use the captured resume command for supported agent tasks | | `X` | Kill a running task (`TERM`, then `KILL` after 2 s), or remove a finished one | | `w` | Save the current tasks as a session | -| `o` | Load a saved session or a recovery snapshot (opens the [session picker](#the-o-session-picker)) | +| `o` | Load a saved session or a recovery snapshot (through the [session picker](#the-o-session-picker)) | | `?` | Open the [controls overlay](#the--controls-overlay) | | `q` (or `Ctrl-C`) | Disconnect from the daemon; under `--foreground`, quit and stop the tasks | | `Q` | Quit; kill the tasks and stop the daemon | @@ -52,160 +52,160 @@ | `✗` | Completed, non-zero exit | | `◆` | Tagged "in use" | -The `∙` glyph and the Idle section both apply after 10 seconds without output. +After 10 seconds without output, a task is marked `∙` and grouped under Idle. ### Input and task lifecycle #### Peek vs. attach -`Space` opens a read-only overlay containing the selected task's live screen. `↑`/`↓` move between tasks without closing the overlay; `Space`, `Esc`, or `q` closes it. `Enter`, from either the dashboard or peek, attaches to the task and forwards input to its PTY. Full-screen programs such as `vim` and `htop` retain their terminal state and cursor. +Press `Space` to peek at the selected task's live screen in a read-only overlay. Use `↑`/`↓` to move between tasks; press `Space`, `Esc`, or `q` to close. Press `Enter` from the dashboard or peek to attach and send input to the task's PTY. Terminal state and cursor position are preserved for full-screen programs such as `vim` and `htop`. #### Attach and background -Attached mode gives the child control of terminal input. `Ctrl-\` returns to the dashboard; other supported keys, including `Ctrl-C`, `Ctrl-Z`, and `Ctrl-D`, are forwarded to the child. +While attached, send terminal input to the child. Press `Ctrl-\` to return to the dashboard; other supported keys, including `Ctrl-C`, `Ctrl-Z`, and `Ctrl-D`, are forwarded to the child. -Cursor-key encoding follows the child's live cursor-key mode. In application-cursor mode, unmodified cursor keys use `SS3` (`ESC O A` for Up); modified cursor keys use `CSI`. Function keys, modified navigation such as `Alt+Left`, and standard `Ctrl` combinations are also supported. +Cursor keys are encoded according to the child's live cursor-key mode. In application-cursor mode, unmodified cursor keys are encoded with `SS3` (`ESC O A` for Up); modified cursor keys are encoded with `CSI`. Function keys, modified navigation such as `Alt+Left`, and standard `Ctrl` combinations are also supported. -`Ctrl-\` names the physical chord. Crossterm may report the same chord as `Ctrl-4`, so `fleetcom` accepts both representations. +`Ctrl-\` is the physical chord. The same chord may be reported by Crossterm as `Ctrl-4`; both representations are accepted. #### Modified keys, paste, and mouse input These inputs cannot all be forwarded byte-for-byte. Their encoding and destination depend on the terminal and the attached child's modes. -Shift+Enter and Alt+Enter send `ESC CR` rather than plain `CR`. If the terminal does not report modified keys, `fleetcom` cannot distinguish Shift+Enter from Enter and forwards plain `CR`. +Press Shift+Enter or Alt+Enter to send `ESC CR` rather than plain `CR`. Without terminal support for reporting modified keys, Shift+Enter cannot be distinguished from Enter and is forwarded as plain `CR`. -Paste travels as one message. For bracketed-paste-aware children, `fleetcom` adds paste markers and removes embedded terminators. Otherwise, it converts line endings to `CR`. Its own text fields strip control characters. +Pasted text is sent as one message. For bracketed-paste-aware children, paste markers are added and embedded terminators removed. Otherwise, line endings are converted to `CR`. Control characters are stripped before insertion into `fleetcom` text fields. -Mouse routing follows the child's reported modes. A mouse-aware child receives clicks, drags, releases, and wheel events in the negotiated encoding. For a full-screen child without mouse reporting, the terminal's alternate-scroll mode handles the wheel when enabled. +Mouse events are routed according to the child's reported modes. Clicks, drags, releases, and wheel events are forwarded in the negotiated encoding when mouse reporting is enabled. For a full-screen child without mouse reporting, use the wheel through alternate-scroll mode when enabled. -Selection depends on who owns mouse input. When `fleetcom` has capture, a left drag selects child-screen text on inline screens, in full-screen programs that disable alternate scroll, and in scrollback. While scrollback is visible, `fleetcom` keeps capture and never forwards mouse events to the child. Release copies the highlighted span to the system clipboard via OSC 52 and shows a `copied N chars` status-bar notice. Trailing padding is trimmed from each selected row, and concealed (SGR 8) cells copy as the blanks shown on screen. +Selection depends on who owns mouse input. With `fleetcom` mouse capture, drag with the left button to select child-screen text on inline screens, in full-screen programs that disable alternate scroll, and in scrollback. While scrollback is visible, mouse capture is retained and mouse events are never forwarded to the child. Release to copy the highlighted span to the system clipboard via OSC 52. The status-bar notice is `copied N chars`. Trailing padding is trimmed from each selected row, and concealed (SGR 8) cells are copied as the blanks shown on screen. -Terminal-native selection remains available under capture through the terminal's override modifier, typically Shift. This selects across the terminal's entire view, including `fleetcom`'s chrome. On the dashboard, in peek, and in full-screen programs using alternate scroll, the terminal owns drag selection directly. +Terminal-native selection remains available under capture through the terminal's override modifier, typically Shift. Use this modifier to select across the terminal's entire view, including `fleetcom`'s chrome. On the dashboard, in peek, and in full-screen programs using alternate scroll, drag selection is handled directly by the terminal. -Attached children do not receive kitty keyboard-protocol or application-keypad sequences. Keypad digits send their normal characters. +Attached children do not receive kitty keyboard-protocol or application-keypad sequences. Keypad digits are sent as their normal characters. #### Scrollback -Each task retains 2,000 lines of scrollback by default. `--scrollback ` or `FLEETCOM_SCROLLBACK` overrides the depth, with the flag taking precedence. Values are capped at 100,000 lines; `0` disables scrollback, and an unparsable environment value falls back to the default instead of failing startup. +By default, 2,000 lines of scrollback are retained per task. Set `--scrollback ` or `FLEETCOM_SCROLLBACK` to override the depth; prefer the flag when both are set. Values are capped at 100,000 lines. Set `0` to disable scrollback. An unparsable environment value is treated as the default without failing startup. -The supervisor reads this setting when it starts. It therefore applies to a `--foreground` run or a daemon started by the current invocation. An existing daemon keeps its configured depth until `fleetcom --kill`. +The depth is configured at supervisor startup: for a `--foreground` run or a daemon started by the current invocation. To change the depth of an existing daemon, stop it with `fleetcom --kill` first. -While attached to an inline child, wheel-up over its output enters scrollback. `Shift+PageUp` also enters it; `Ctrl+PageUp` and `Alt+PageUp` provide alternatives when the terminal intercepts Shift. The status bar shows the current offset as `[scroll ↑N]`. +While attached to an inline child, scroll up over its output or press `Shift+PageUp` to enter scrollback. Use `Ctrl+PageUp` or `Alt+PageUp` if Shift is intercepted by the terminal. Read the current offset in the status bar as `[scroll ↑N]`. -Once open, the wheel scrolls by three lines. `PageUp` and `PageDown` move by pages, `↑` and `↓` move by one line, and `Home` jumps to the oldest retained row. A left drag selects the displayed history; release copies it using the same trimming and concealment rules as live-screen selection. Scrolling cancels an active drag. +In scrollback, use the wheel to scroll by three lines, `PageUp`/`PageDown` by pages, `↑`/`↓` by one line, or `Home` to reach the oldest retained row. Drag with the left button to select displayed history; release to copy it using the same trimming and concealment rules as live-screen selection. An active drag is canceled on scroll. -`Esc`, `Enter`, `q`, `End`, or reaching the bottom returns to live output. Typing returns to live output and forwards the key to the child; `Ctrl-\` backgrounds the task as usual. Leaving scrollback cancels an active drag, while detaching or switching tasks resets the view. +Press `Esc`, `Enter`, `q`, or `End`, or scroll to the bottom, to return to live output. Type to return to live output and send the key to the child; press `Ctrl-\` to background the task as usual. An active drag is canceled on leaving scrollback. The view is reset on detach or task switch. #### Destroying tasks -Destroy is Shift-gated: only uppercase `X` acts. It kills a running task or removes a finished one. Removal also terminates remaining processes in the task's process group, escalating from `TERM` to `KILL` after two seconds. Lowercase `x` and `Ctrl-X` do nothing. +Press uppercase `X` to kill a running task or remove a finished one. On removal, remaining processes in the task's process group are also terminated: `TERM`, then `KILL` after two seconds. Lowercase `x` and `Ctrl-X` are unbound. #### Rerunning tasks -`r` acts only on a finished task. A running task remains untouched because rerunning it would first require a destructive kill. +Press `r` to rerun a finished task. Running tasks cannot be rerun without first being killed and are left untouched. -The replacement starts in the same directory using the requesting client's environment. Most tasks reuse their stored command. A supported `claude`, `codex`, `grok`, or `omp` task instead uses its captured resume command when a valid conversation ID is available. +`fleetcom` starts the replacement in the same directory, using the requesting client's environment and the stored command. For a supported `claude`, `codex`, `grok`, or `omp` task with a valid captured conversation ID, it uses the resume command instead. -Rerunning preserves the task's ID, `◆` tag, group, name, and spawn order; its clock and screen reset. Since lifecycle affects sorting, the task may move to another section when it starts. The same key works inside peek, which remains open while the replacement starts. +On rerun, the task's ID, `◆` tag, group, name, and spawn order are preserved; its clock and screen are reset. Under lifecycle sorting, the restarted task may be listed in another section. You can also rerun inside peek without closing the overlay. #### Disconnecting and quitting -Input meaning depends on the active surface. From the dashboard, `q` or `Ctrl-C` disconnects the client. A daemon and its tasks continue running, so the next `fleetcom` invocation reconnects. Under `--foreground`, the in-process core exits with the client and its tasks die. While attached, `Ctrl-C` belongs to the child. In prompts and pickers, `Esc` cancels without disconnecting. +Input meaning depends on the active surface. From the dashboard, press `q` or `Ctrl-C` to disconnect. A daemon and its tasks continue running, so you can reconnect with `fleetcom`. Under `--foreground`, the in-process core exits with the client and its tasks are terminated. While attached, `Ctrl-C` belongs to the child. In prompts and pickers, press `Esc` to cancel without disconnecting. -Uppercase `Q` stops the daemon and terminates each task's process group. Shutdown sends `TERM` first, then `KILL` after one shared two-second grace period. Exited leaders remain unreaped until escalation so `TERM`-ignoring descendants in their groups still receive `KILL`. A nonempty fleet waits the grace even when its listed tasks have finished or respect `TERM`; with no tasks left to clean up, shutdown returns immediately. Tasks already terminating keep their original escalation timers. Processes that have moved into another group or session are outside this sweep. +Press uppercase `Q` to stop the daemon and terminate each task's process group: `TERM` first, then `KILL` after one shared two-second grace period. Exited leaders remain unreaped until escalation so `TERM`-ignoring descendants in their groups still receive `KILL`. For a nonempty fleet, shutdown is delayed by the grace period even when all listed tasks have finished or exited on `TERM`; with no tasks left to clean up, shutdown is immediate. Original escalation timers are retained for tasks already terminating. Processes that have moved into another group or session are outside this sweep. ### Task organization #### Grouping and tagging -`s` cycles three grouping modes: state, dir, custom. The header shows the strip `by state · dir · custom` with the active mode bold and the rest dim. +Press `s` to cycle through state, dir, and custom grouping. In the header strip `by state · dir · custom`, the active mode is bold and the rest dim. -- By state: In use / Running / Idle / Completed. A running task files under Idle after 10 s without output; Completed stays one section (`✓`/`✗` show exit status). +- By state: In use / Running / Idle / Completed. After 10 s without output, a running task is grouped under Idle. Completed tasks are grouped in one section, with exit status marked `✓`/`✗`. - By dir: one section per working directory; the invocation directory first, then the remaining labels sorted without regard to case. - By custom group: one section per group name, sorted without regard to case, with Unassigned last. Fresh spawns remain unassigned unless they inherit a group, and the Unassigned section exists only while it has a member. -Dashboard section labels and within-section directory tiebreaks use the same case-insensitive order. Names that differ only by case sort next to each other in a deterministic order. Group identity remains case-sensitive, so `API` and `api` stay separate sections. +Dashboard section labels and within-section directory tiebreaks use the same case-insensitive order. Names that differ only by case are sorted next to each other in a deterministic order. Group identity remains case-sensitive, so `API` and `api` stay separate sections. -Groups belong to task state: an assignment survives client detach and rerun (`r`), and switching grouping modes does not modify it. `g` reassigns the selected task through the [group picker](#the-g-group-picker). +Groups belong to task state: an assignment survives client detach and rerun (`r`), and switching grouping modes does not modify it. Press `g` to reassign the selected task through the [group picker](#the-g-group-picker). -`m` toggles the "in use" tag and marks the task with `◆`. In state mode, tagged tasks form the In use section at the top. In custom mode, a tag moves the task to the top of its existing group rather than creating a global section. Within a dir or custom section, tasks sort as tagged, live, then completed; each class then sorts by directory and spawn order. Idle state does not affect row order in these modes, so a quiet task keeps its position and shows `∙`. State mode instead moves quiet tasks from Running to Idle. +Press `m` to toggle the "in use" tag, marked `◆`. In state mode, tagged tasks form the In use section at the top. In custom mode, tagged tasks are placed at the top of their existing groups rather than in a global section. Within a dir or custom section, tasks are sorted as tagged, live, then completed; each class is then sorted by directory and spawn order. Idle state does not affect row order in these modes, so quiet tasks are kept in place and marked `∙`. In state mode, quiet tasks are moved from Running to Idle. -`M` cycles the selection through tagged tasks in dashboard order. It wraps after the last tagged task. With no tagged tasks, the selection does not move; with one, the selection moves to that task and stays there. +Press `M` to select the next tagged task in dashboard order, wrapping after the last. With no tagged tasks, the selection is unchanged; with one, that task is selected. -In custom mode only, a new command inherits the selected task's group, through both `n` and the `@` picker. The spawn prompt shows the destination as `❯ dir ▸ group ▸ command`, each segment present only when it applies: the dir segment for a non-default directory, the group segment when a group will be inherited. State- and dir-mode spawns start unassigned. +In custom mode only, a new command is assigned the selected task's group, through both `n` and the `@` picker. The destination is shown in the spawn prompt as `❯ dir ▸ group ▸ command`, each segment present only when it applies: the dir segment for a non-default directory, the group segment when a group will be inherited. New tasks are unassigned in state and dir modes. #### Renaming -`R` opens a rename prompt containing the selected task's current name. `Enter` saves; an empty field clears the name and restores the command as the display label; `Esc` cancels. Plain `r` remains rerun. +Press `R` to rename the selected task, starting with its current name. Press `Enter` to save, or `Esc` to cancel. Leave the field empty to clear the name and display the command again. Use lowercase `r` to rerun. -A named task shows its name in place of the command in the dashboard row and the peek title. The attached status bar shows `name · command`. +For a named task, the name is displayed in place of the command in the dashboard row and peek title. In the attached status bar, both are displayed as `name · command`. -The daemon removes control characters, trims surrounding whitespace, and limits display names to 64 characters. An empty result clears the name. Unlike groups, `Unassigned` is a legal display name; only the [group picker](#the-g-group-picker) reserves that label. +The daemon removes control characters and surrounding whitespace from display names, then limits them to 64 characters. An empty result means no name. Unlike groups, `Unassigned` is a legal display name; the label is reserved only in the [group picker](#the-g-group-picker). ## The `@` directory picker -`@` opens a bottom panel containing a path field and its matching directories. `Enter` depends on the selected row type: +Press `@` to open the directory picker: a bottom panel with a path field and matching directories. Use `Enter` according to the selected row type: - Resolved path: run the command in that directory (`Enter`). Row 0 is always this row, so the list is never empty. -- Current task directories: `Enter` runs there; `Tab`/`→` browses into them. These rows precede subdirectories. -- Subdirectories of the resolved path: `Enter` or `Tab`/`→` descends into one. +- Current task directories: press `Enter` to run there, or `Tab`/`→` to browse into them. These rows precede subdirectories. +- Subdirectories of the resolved path: press `Enter` or `Tab`/`→` to descend into one. -Typing filters both lists under different rules. A subdirectory matches the fragment as a case-insensitive prefix. A current task directory matches a case-insensitive substring of its final path component: `log` finds `~/Documents/Code/Rust/Logria`, while `crab` finds both `crabapple` and `crabstep`. Parent components do not participate, so `doc` does not match every directory under `~/Documents/`. +Typing filters both lists under different rules. A subdirectory matches the fragment as a case-insensitive prefix. A current task directory matches a case-insensitive substring of its final path component: type `log` to find `~/Documents/Code/Rust/Logria`, or `crab` to find both `crabapple` and `crabstep`. Parent components do not participate, so `doc` does not match every directory under `~/Documents/`. -Once the field contains `/`, current task directory rows are omitted; the picker shows the resolved path and its matching subdirectories. Without `/`, a current task directory that is also a matching subdirectory appears once, with the current task row behavior. +Once the field contains `/`, current task directory rows are omitted; only the resolved path and matching subdirectories are shown. Without `/`, a current task directory that is also a matching subdirectory appears once, with the current task row behavior. -`Backspace` deletes one character and the matches re-filter; `↑`/`↓` move the highlight; `Esc` cancels. Completion updates on each input, permitting navigation and launch without leaving the dashboard. `←`/`→` move the caret within the typed path (`→` descends only when the caret is at the end), and `Ctrl-A`/`Ctrl-E` (or `Home`/`End`) jump to either end; the same caret keys work in every `fleetcom` text field. +Press `Backspace` to delete one character, `↑`/`↓` to move the highlight, or `Esc` to cancel. Matches are refreshed on each input. Use `←`/`→` to move the caret within the typed path (`→` to descend only at the end), and `Ctrl-A`/`Ctrl-E` (or `Home`/`End`) to jump to either end. Use the same caret keys in every `fleetcom` text field. ## The `/` find palette -`/` opens a bottom panel listing tasks that match the query. Each row reads `