Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Organize related tasks into named groups across working directories.

### Resume agent sessions

Start `claude`, `codex`, `grok`, or `omp` normally. Rerun the task or reload a saved session to resume the conversation.
Launch `claude`, `codex`, `grok`, or `omp` from the Agent page (`n`, then `Tab`). Rerun the task or reload a saved session to resume the conversation.

## Operational model

Expand All @@ -40,7 +40,7 @@ Managing several long-lived commands across terminal panes is pesky, especially
- 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.
- Launch `claude`, `codex`, `grok`, and `omp` as managed agents and resume their conversations on rerun and reload.
- Recover the current task set from automatic snapshots.

## Documentation
Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ Full command lines are persisted in recipes, including any embedded secrets. A t

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 in shell commands
### Captured IDs

To resume an agent conversation, insert its captured ID into a command run through `$SHELL -c`. Before shell insertion, accept only lowercase hexadecimal in the `8-4-4-4-12` UUID format. Validate capture payloads, Codex rollout headers, live session records, and the final command construction. Never read session IDs from terminal output; instrument only a bare program word or its canonical resume form. See [Agent session resume](agent-resume.md#validation-boundary) for both boundaries.
Pass a captured ID as one agent argv element and store it as the session file's `resume` field; never insert it into a shell command. Before either use, validate it as lowercase hexadecimal in the `8-4-4-4-12` UUID format. Validate capture payloads, Codex rollout headers, live session records, and session files on load. Never read session IDs from terminal output. Enable capture only for managed agents launched from the Agent page or loaded from managed session entries; run typed commands unchanged without a capture channel. See [Agent session resume](agent-resume.md#validation-boundary).

### Copying text through the terminal

Expand Down
109 changes: 56 additions & 53 deletions docs/agent-resume.md

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Choose the operating mode with launch arguments. Use keys to control the dashboa
| `fleetcom --help` / `-h` | Print usage and exit |
| `fleetcom --version` / `-V` | Print the version and exit |

`--daemon` is internal. The daemon is started automatically when needed. The first non-`-` argument is treated as the session name; a second is rejected.
`--daemon` and `--codex-notify-v1` are internal. The daemon is started automatically when needed; the notify mode is run by the notifier injected into managed `codex` tasks, see [agent session resume](agent-resume.md#codex). The first non-`-` argument is treated as the session name; a second is rejected.

## Dashboard

Expand All @@ -24,8 +24,8 @@ Choose the operating mode with launch arguments. Use keys to control the dashboa
| `Tab` / `Shift-Tab` | Jump the selection to the next / previous section |
| `Enter` | Attach to the selected task |
| `Space` | Peek at the selected task |
| `n` | New command in the invocation directory |
| `@` | New command in a directory you pick |
| `n` | New command or agent in the invocation directory (through the [spawn prompt](#the-n-spawn-prompt)) |
| `@` | New command or agent in a directory you pick |
| `/` | 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) |
Expand All @@ -34,7 +34,7 @@ Choose the operating mode with launch arguments. Use keys to control the dashboa
| `Ctrl-]` | Attach to the flagship |
| `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; use the captured resume command for supported agent tasks |
| `r` | Rerun a finished task; resume the conversation for a managed agent |
| `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 (through the [session picker](#the-o-session-picker)) |
Expand Down Expand Up @@ -111,7 +111,7 @@ Press uppercase `X` to kill a running task or remove a finished one. On removal,

Press `r` to rerun a finished task. Running tasks cannot be rerun without first being killed and are left untouched.

`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.
On rerun, the replacement is started in the same directory with the requesting client's environment. For a literal task, the exact command text is run again. For a managed agent, the binary is resolved on the current `PATH`, and the captured conversation is resumed when an ID is known; see [agent session resume](agent-resume.md).

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.

Expand Down Expand Up @@ -162,6 +162,14 @@ For a named task, the name is displayed in place of the command in the dashboard

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 `n` spawn prompt

Press `n` to open the spawn prompt in the invocation directory, or pick a directory first with `@`. On the Command page, enter text to run unchanged through your shell. Press `Tab` or `Shift-Tab` for the Agent page, where the supported CLIs found on the launch `PATH` are listed in a fixed order. Select an agent and press `Enter` to start a managed task, with argv and capture configured by `fleetcom`; see [agent session resume](agent-resume.md).

Type a case-insensitive substring of a program name to filter the list: for example, `x` to find `codex`. The first match is highlighted. Use `↑`/`↓` to select, `Enter` to launch in the prompt's directory and group, or `Esc` to cancel. If nothing matches, the missing supported agents are listed in the prompt.

Your command and filter text are preserved separately across page changes. Each time you open the prompt, the filter is cleared, so you can press `Tab` to view the full agent list. With no agents found, `Tab` is unbound and omitted from the hint.

## The `@` directory picker

Press `@` to open the directory picker: a bottom panel with a path field and matching directories. Use `Enter` according to the selected row type:
Expand Down
50 changes: 27 additions & 23 deletions docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,21 @@ The filename is derived from the session name: leading and trailing whitespace i

## Format

A session file is a JSON object with three fields. `version` identifies the format version, currently 1. `name` holds the session name as typed, trimmed but not sanitized. `dirs` maps each working directory to an ordered list of entries. An entry with neither a group nor a name is a command string. An entry carrying either is an object with `cmd` plus the optional `group` and `name` fields:
A session file is a JSON object with three fields: `version` for the format version, currently 2; `name` for the typed session name, trimmed but not sanitized; and `dirs` for an ordered list of entries per working directory. Use one of two entry forms, according to the task's launch mode:

- A **literal** entry is a typed command. Use a bare string without labels, or an object with `cmd` and optional `group` and `name` fields. On load, the stored text is run unchanged through your shell: even `"claude"` is a literal command.
- A **managed** entry is an object with `agent`, the registered program word (`claude`, `codex`, `grok`, or `omp`), plus optional `resume`, `group`, and `name` fields. Tasks launched from the Agent page are saved in this form. On load, the agent is launched as managed; with `resume`, the stored session ID is reopened, and without it, a fresh conversation is started. See [Agent session resume](agent-resume.md).

```json
{
"version": 1,
"version": 2,
"name": "work/api",
"dirs": {
"~/work/api": [
"cargo watch -x test",
{ "cmd": "cargo run", "group": "api", "name": "api server" }
{ "cmd": "cargo run", "group": "api", "name": "api server" },
{ "agent": "claude", "resume": "c8c4a5cc-0b32-4ba0-a6b4-6ed08c218e0d", "group": "agents" },
{ "agent": "omp" }
],
"/tmp": [
"top"
Expand All @@ -38,43 +43,42 @@ A session file is a JSON object with three fields. `version` identifies the form

- The stored `name` distinguishes session names that sanitize to the same filename: `a/b` and `a.b` both become `a_b.json`. Before saving, `fleetcom` compares the stored and incoming names and refuses a mismatch, reporting both names in the error. The load picker displays the stored name as `a/b`, not `a_b`.
- Keys under `dirs` are directory paths: each task's working directory. On save, directories under `$HOME` are written as `~/...`; other paths are kept absolute. On load, `~` is expanded to `$HOME`, and relative keys are resolved against the invocation directory of the client loading the session.
- Values are ordered lists. A string member is a bare shell command; in the object form, an optional group and display name can also be specified for assignment on load. Order is preserved, and each command is run in its own PTY under that directory.
- Directories are serialized alphabetically. Command order remains stable within each directory.
- Values are ordered lists, loaded in order. Each task is started in its own PTY under the specified directory.
- Directories are serialized alphabetically, with entry order preserved within each directory.

The `version` field must be an integer from 1 through the newest format supported by the running `fleetcom`. A missing field means version 1. Invalid or unsupported versions are refused on load, with the file's value and supported version in the error.
The `version` field must be an integer from 1 through the newest format supported by the running `fleetcom`. A missing field means version 1. Invalid or unsupported versions are refused on load, with the file's value and supported version in the error. A version 2 file read by an older `fleetcom` is refused by that same gate.

The schema is identified by shape, independently of the version. With an object-valued `dirs`, the wrapped form above is used. A flat map is also accepted, with directories as top-level keys and entry arrays as values. In that form, an array-valued key named `dirs` remains a directory entry, but a top-level `version` member is always the format version, never a directory. Flat-map files are listed by filename stem because no name is stored. On save, the wrapped form is written, without a stored-name collision check.

Saves are atomic: a private temporary file is written and synced in the session directory, then renamed over the recipe. Full command lines are persisted, including any embedded secrets. See [Security](README.md#security) for directory and file permissions.

The file is plain JSON and practical to edit by hand. Edit `name` to change the stored session identity: a subsequent save under the old name will be refused by the collision check. A readable stored name is still used for collision checks and picker labels when the command body is invalid or the version unsupported.

The entire recipe is validated before any commands are started. The root must be an object, every directory value must be an array, and every entry must be a command string or an object with a string `cmd`. Strings, `null`, or omission are accepted for optional entry `group` and `name` fields and the wrapped session's `name`; other types are rejected. Unknown fields in wrapped metadata and entry objects are ignored. Empty maps, arrays, and strings are valid. With invalid JSON or any malformed field, the whole load is refused; existing tasks and the recipe file are left intact. In schema errors, the quoted directory key, entry number (starting at 1), and offending field are included where applicable.
The entire recipe is validated before any commands are started. The root must be an object, every directory value must be an array, and every entry must be a command string or an object with a string `cmd` or a string `agent`, never both. `agent` must be a supported program word, and `resume`, when present, must be a lowercase `8-4-4-4-12` UUID or `null`. Strings, `null`, or omission are accepted for optional entry `group` and `name` fields and the wrapped session's `name`; other types are rejected. Unknown fields in wrapped metadata and entry objects are ignored. Empty maps, arrays, and strings are valid. With invalid JSON or any malformed field, the whole load is refused; existing tasks and the recipe file are left intact. In schema errors, the quoted directory key, entry number (starting at 1), and offending field are included where applicable.

After validation, group and display names are stripped of control characters and surrounding whitespace and limited to 64 characters. `Unassigned` is treated as no group but is a legal display name. Missing directories and entries exceeding task or command limits are still skipped and counted in the load status. Spawn failures are reported separately; commands already started by a structurally valid recipe keep running.
After validation, group and display names are stripped of control characters and surrounding whitespace and limited to 64 characters. `Unassigned` is treated as no group but is a legal display name. Missing directories, entries past the task limit, and literal commands past the length limit are skipped and counted in the load status. A managed entry whose agent is not found on the connected client's `PATH` is reported on the status line and counted as a failed spawn; the remaining entries are still loaded. Spawn failures are reported separately; tasks already started from a structurally valid recipe are left running.

Use the string form for commands with neither a group nor a name. String and object entries can appear in the same directory array.

A bare agent command specifies no conversation ID, so saving it verbatim would start a new conversation on load. When `fleetcom` has captured an ID for `claude`, `codex`, `grok`, or `omp`, it stores the resume form instead. Without a known ID, it preserves the authored command in named saves and recovery snapshots. You can run the resulting command directly in a shell:
### Loading version 1 files

```json
{
"version": 1,
"name": "agents",
"dirs": {
"~/work/api": [
"claude --resume 'c8c4a5cc-0b32-4ba0-a6b4-6ed08c218e0d'",
{ "cmd": "codex resume '019f5453-de22-7240-b2e5-0d32692aa6d9'", "name": "reviewer" }
]
}
}
```
In format 1, instrumented tasks were stored as command strings: a bare program word, or the word followed by its resume selector and a single-quoted UUID. When loading a file with `version` 1 or no version field, convert exactly those two forms to managed entries:

| Version 1 entry | Loads as |
| -- | -- |
| `claude`, `codex`, `grok`, or `omp` alone | managed, fresh conversation |
| `claude --resume '<uuid>'`, `codex resume '<uuid>'`, `grok --resume '<uuid>'`, or `omp --resume '<uuid>'`, with or without the quotes | managed, resuming that ID |
| anything else | literal, unchanged |

Tabs and repeated spaces between tokens are still accepted. Commands with paths (`/usr/local/bin/claude`, `~/bin/claude`) are loaded as literals to preserve your chosen binary instead of substituting one found on `PATH`; managed resume tracking is then unavailable. Commands with extra arguments, prompts, other resume spellings, or shell syntax remain literal, as before. No conversion is applied to version 2 files: a literal `claude` stays literal.

Recovery snapshots are rewritten continually in the new format; named sessions are converted on the next save. This conversion will be removed one release after format 2 ships. After that, all entries in unconverted version 1 files will be loaded as literals.

## Recovery

The current task set is automatically snapshotted under `recovery/` inside the session directory. A separate file is written for each daemon (or `--foreground` core), named for its start time and process ID. After each write, the new file and files whose process IDs are still live are protected; among the remaining files, the nine newest names are retained. A shared recovery directory can therefore contain more than ten snapshots while multiple writers are live.

A snapshot pass is scheduled two seconds after the last command that can change a saved recipe, coalescing a burst of commands. A nonempty recipe is written when its content or destination changed, or when the expected snapshot file is missing. Stored-command changes, such as a newly captured agent resume ID, are also checked every 60 seconds.
A snapshot pass is scheduled two seconds after the last command that can change a saved recipe, coalescing a burst of commands. A nonempty recipe is written when its content or destination changed, or when the expected snapshot file is missing. Content changes that no command caused, such as a managed task's newly captured session ID, are also checked every 60 seconds.

- No snapshot is written for an empty fleet. After removing every task, the previous snapshot is retained.
- Snapshots are left in place after quitting, disconnecting, or `fleetcom --kill`.
Expand All @@ -87,7 +91,7 @@ As with saved recipes, full command lines are persisted in recovery files, inclu

## Saving and loading

- Save: press `w` in the dashboard, type a name, and press `Enter`. This writes the session name plus each task's directory, command, and optional group and name to `<name>.json`.
- Save: press `w` in the dashboard, type a name, and press `Enter` to write `<name>.json` with the session name and each task's directory, command or managed agent and session ID, and optional group and name.
- Load in-app: `o`, pick from the list, `Enter`.
- Load at launch: `fleetcom <name>`.

Expand Down
Loading
Loading