From 940ca13c19ce3b5616f63681c39285c9dfe7eed3 Mon Sep 17 00:00:00 2001 From: Ferrol Aderholdt Date: Thu, 23 Jul 2026 08:55:13 -0700 Subject: [PATCH] docs: refactor README into overview + per-feature manuals Reframe the README around linkshell's orchestration identity instead of "agent-aware tmux": lead with the resident orchestrator and agent coordination as a control plane on top of the terminal grid, then the awareness a generic multiplexer can't provide. Split the monolithic 526-line README into a lean overview that links to focused per-feature guides under docs/: sessions, panes-and-navigation, pipes, councils, chat, orchestrator, and agent-integration. Update CLAUDE.md's documentation map and repoint the keybindings/command-bar source-of-truth note to docs/panes-and-navigation.md. --- CLAUDE.md | 11 +- README.md | 525 +++++------------------------------ docs/agent-integration.md | 108 +++++++ docs/chat.md | 55 ++++ docs/councils.md | 21 ++ docs/orchestrator.md | 90 ++++++ docs/panes-and-navigation.md | 104 +++++++ docs/pipes.md | 23 ++ docs/sessions.md | 128 +++++++++ 9 files changed, 600 insertions(+), 465 deletions(-) create mode 100644 docs/agent-integration.md create mode 100644 docs/chat.md create mode 100644 docs/councils.md create mode 100644 docs/orchestrator.md create mode 100644 docs/panes-and-navigation.md create mode 100644 docs/pipes.md create mode 100644 docs/sessions.md diff --git a/CLAUDE.md b/CLAUDE.md index eb2c5ce..9d2c997 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,11 +93,18 @@ Skills are `*.md` files in `~/.config/linkshell/skills/` (name + description in Vertical panes: main output (optionally split into two session panes), session bar, status panel, and an optional chat pane (`alt-t`, dockable with `alt-g`). Overlays: NewSession dialog, command bar/palette (`alt-c`), pipes overlay, help (`alt-h`). -Keybindings and command-bar commands are user-facing — keep the README's Keybindings and Command Bar sections as the source of truth and update them when defaults in `keybindings.rs` or the parser in `app.rs::execute_command` change. +Keybindings and command-bar commands are user-facing — keep the Keybindings and Command Bar sections in `docs/panes-and-navigation.md` as the source of truth and update them when defaults in `keybindings.rs` or the parser in `app.rs::execute_command` change. ## Documentation -- `README.md` — user-facing feature docs; keep in sync with behavior changes +- `README.md` — user-facing overview; links out to the per-feature guides below +- `docs/sessions.md` — sessions, detach/reattach, profiles, aliased/local agents, states +- `docs/panes-and-navigation.md` — split panes, scrollback, status panel, keybindings, command bar +- `docs/pipes.md` — session pipes +- `docs/councils.md` — multi-agent councils +- `docs/chat.md` — agent chat pane and local LLM agents +- `docs/orchestrator.md` — resident orchestrator agent +- `docs/agent-integration.md` — linkshell-ctl, capabilities, hooks, remote agents - `docs/config-reference.md` — full linkshell.toml reference - `docs/recipes.md` — workflow recipes - `docs/orchestrator-memory.md` — orchestrator memory design diff --git a/README.md b/README.md index b1f3ec3..68aaf01 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # ◈ linkshell -A terminal multiplexer built for AI coding agents. Run Claude, Codex, and shell sessions side by side — and actually know what each one is doing. - -See the [workflow recipes](docs/recipes.md), [configuration reference](docs/config-reference.md), -and [changelog](CHANGELOG.md). +An orchestration workspace for AI coding agents. It starts where tmux does — +Claude, Codex, and shell sessions side by side in one terminal — and keeps +going: it knows what each agent is doing, lets them talk to each other, routes +output between them, and can run a resident agent that drives the whole board. ``` ┌─────────────────────────────────────────────────────────────────┐ @@ -23,35 +23,57 @@ and [changelog](CHANGELOG.md). └─────────────────────────────────────────────────────────────────┘ ``` -## Why - -tmux doesn't know your Claude session is blocked waiting on you. It doesn't know your Codex session just hit an error. It can't tell you token usage or cost at a glance. Linkshell does. - -## Features - -- **Up to 8 sessions** — Claude, Codex, shell, or any custom command -- **Detach, reattach & multiple sessions** — tmux/screen-style client/server split; quit the client and sessions keep running. Run many independent linkshells (`linkshell ls` to list, `linkshell -r ` to reattach) -- **Recursive split panes** — split any pane side by side (`alt-\`) or top/bottom (`alt--`), repeatedly and in any direction, for arbitrary tiled layouts; `alt-w` closes a pane, `alt-r` rotates a split, `alt-o` switches focus -- **Startup profiles** — save a layout of sessions and pipes with `profile save `, relaunch it with `--profile ` -- **Aliased identities** — env-prefixed commands (`CLAUDE_CONFIG_DIR=~/w claude`) and configured wrapper aliases get full Claude/Codex treatment, each with its own config home -- **Local agents & LLMs** — opencode, oh-my-pi, pi, aider, and llama.cpp sessions get agent-style state inference; llama.cpp/Ollama/vLLM/LM Studio endpoints are chat-addressable via `[agents.*]` -- **Agent chat pane** (`alt-t`) — talk to any session or local LLM by name, run commands with `/`, and orchestrate everything without leaving the pane -- **Unified scrollback** — full-screen TUIs and shells scroll with the same keys; the view holds position while output streams and returns to live when you type -- **Multi-agent councils** — declarative TOML topologies that relay output between agents on state transitions, with fan-in joins, round limits, and done signals -- **Live session state** — READY, THINKING, RUNNING, WAITING, ERROR inferred from PTY output and JSONL logs -- **Accurate token & cost tracking** — read directly from the Claude/Codex JSONL logs (config-home aware, `CLAUDE_CONFIG_DIR`/`CODEX_HOME` respected), not screen-scraped -- **Pro/Max subscription aware** — detects subscription automatically; shows real token counts, skips meaningless cost -- **Session pipes** — forward output between sessions on state change; flash indicator when a pipe fires -- **Agent communication** — sessions receive `LINKSHELL_SESSION_ID`, `LINKSHELL_SOCK`, and a per-session `LINKSHELL_TOKEN` at spawn; use `linkshell-ctl` to signal state, wait for READY, or trigger pipes from within a session -- **Capability security** — every IPC connection is scoped: human shells hold operator rights, AI agents get worker capabilities, council members can only report state; TCP requires a valid token -- **Remote agent support** — opt-in TCP listener for agents on other machines; same typed JSONL protocol as the Unix socket -- **WAITING alerts** — yellow border when an agent is blocked on your input -- **Desktop notifications** — configurable WAITING/ERROR alerts via notify-send, OSC 9, or bell -- **ERROR alerts** — red flashing border on failure -- **Mouse text selection** — drag to select, auto-copies to clipboard -- **Centered session bar** — slots reflow based on how many sessions are open -- **Full PTY passthrough** — your keystrokes go straight to the active session -- **Color coded by type** — 🟠 orange for Claude, 🔵 blue for Codex +## Why linkshell + +A multiplexer displays your sessions. Linkshell *coordinates* them. It starts as +a familiar tmux-style workspace — but tmux doesn't know your Claude session is +blocked waiting on you, that your Codex session just hit an error, or what any of +it is costing. Linkshell was built from the ground up around AI agents, so the +terminal grid is just the substrate. On top of it sits a control plane: + +- **A resident orchestrator that runs the board.** Tell it "start a claude + session in ~/proj and fix the parser bug" from chat — it spawns, prompts, + waits, and pipes for you, and wakes proactively when something is blocked. + [→ orchestrator](docs/orchestrator.md) +- **Agents that coordinate.** A typed IPC protocol with scoped capabilities lets + sessions signal state, wait on each other, wire pipes, and spawn sessions — + locally or over TCP. [Pipes](docs/pipes.md) route a snapshot from one agent to + another on state change; [councils](docs/councils.md) wire whole author/critic + topologies declaratively. [→ agent integration](docs/agent-integration.md) + +And the awareness a generic multiplexer can't give you: + +- **It knows what each agent is doing.** Live per-session state — READY, + THINKING, RUNNING, WAITING, ERROR — inferred from PTY output and JSONL logs, + with a yellow border when an agent is blocked on you and a red flash on error. + [→ session states](docs/sessions.md#session-states) +- **It tracks real tokens and cost.** Read directly from the Claude/Codex JSONL + logs (config-home aware), not screen-scraped — and subscription-aware, so + Pro/Max shows real token counts and skips meaningless cost. + [→ status panel](docs/panes-and-navigation.md#status-panel) +- **One chat pane drives everything.** Talk to any session, local LLM, or the + orchestrator by name; answer permission prompts; run commands — without + leaving the pane. [→ agent chat](docs/chat.md) + +Plus the multiplexer fundamentals done right: detach/reattach with sessions that +survive, recursive tiled split panes, unified scrollback across full-screen TUIs +and shells, and mouse selection everywhere. + +## Feature guides + +| Guide | What's inside | +|-------|---------------| +| [Sessions](docs/sessions.md) | Starting sessions, detach/reattach, multiple linkshells, startup profiles, aliased Claude/Codex, local agents, session states | +| [Panes & navigation](docs/panes-and-navigation.md) | Split panes, scrollback, status panel, keybindings, command bar | +| [Pipes](docs/pipes.md) | Edge-triggered output forwarding between sessions | +| [Councils](docs/councils.md) | Declarative multi-agent topologies | +| [Agent chat](docs/chat.md) | The chat pane, local LLM agents, permission prompts | +| [Orchestrator](docs/orchestrator.md) | The resident agent: providers, skills, memory, runtime control | +| [Agent integration](docs/agent-integration.md) | `linkshell-ctl`, capabilities, Claude Code hooks, remote agents | +| [Configuration reference](docs/config-reference.md) | Full linkshell.toml reference | +| [Workflow recipes](docs/recipes.md) | End-to-end workflows | +| [Orchestrator memory](docs/orchestrator-memory.md) | Memory design | +| [Changelog](CHANGELOG.md) | Release notes | ## Install @@ -59,9 +81,8 @@ tmux doesn't know your Claude session is blocked waiting on you. It doesn't know cargo install linkshell # installs linkshell + linkshell-ctl ``` -Or grab a prebuilt binary for Linux (x86_64/aarch64) or macOS -(Intel/Apple Silicon) from the -[releases page](https://github.com/wfaderhold21/linkshell/releases). +Or grab a prebuilt binary for Linux (x86_64/aarch64) or macOS (Intel/Apple +Silicon) from the [releases page](https://github.com/wfaderhold21/linkshell/releases). From source: @@ -87,437 +108,15 @@ linkshell --council examples/council.toml # launch a multi-agent council linkshell --profile ucc-dev # launch a named session profile ``` -Then create your first session with `alt-n`. - -Linkshell runs as a client/server pair, like tmux/screen: each `linkshell` -starts a background server that owns its sessions, and the foreground TUI is -a client attached to it. `alt-d` detaches — sessions keep running. - -You can run **multiple independent linkshells** on one machine, screen-style: - -```bash -linkshell # start a new detached server and attach -linkshell new work # start a new one named "work" -linkshell ls # list detached sessions (id, name, pid, status) -linkshell -r # reattach to a specific session by id -linkshell -r # reattach when exactly one session is running -``` - -Each server has its own id, pid, and sockets; `linkshell ls` prunes any whose -process has died. +Then create your first session with `alt-n`. Linkshell runs as a client/server +pair, like tmux/screen: each `linkshell` starts a background server that owns its +sessions, and the foreground TUI is a client attached to it. `alt-d` detaches — +sessions keep running. See the [sessions guide](docs/sessions.md) for detach, +reattach, and running multiple independent linkshells. If something looks wrong (missing logs, stale socket, nested multiplexer, limited terminal colors), run `linkshell doctor` for a diagnostic report. -## Aliased Claude / Codex sessions - -Sessions running Claude or Codex under a different config home are recognized -and get the full treatment — state inference patterns, the JSONL token/cost -watcher, and pricing — instead of being treated as generic custom commands. - -Two ways to spell them: - -**Inline env prefix** — just works, no config needed. The classifier sees -through leading `VAR=value` assignments, and the watcher reads the config -home from the command itself: - -``` -new custom CLAUDE_CONFIG_DIR=~/.claude-work claude -new custom CODEX_HOME=~/.codex-personal codex -``` - -(or enter the same command line in the `alt-n` dialog's Custom field) - -**Config alias** — for wrapper scripts or shell aliases whose name doesn't -contain `claude`/`codex`. Map the command basename in `[sessions.aliases]`: - -```toml -[sessions.aliases.claude-work] -kind = "claude" -config_dir = "~/.claude-work" # exported as CLAUDE_CONFIG_DIR - -[sessions.aliases.cx] -kind = "codex" -config_dir = "~/.codex-personal" # exported as CODEX_HOME -``` - -When `config_dir` comes from an alias it is also injected into the session's -environment, so the CLI and the log watcher agree on where the config home is. -Precedence for the watcher: inline env prefix → alias `config_dir` → -`$CLAUDE_CONFIG_DIR`/`$CODEX_HOME` in linkshell's own environment → the -default `~/.claude` / `~/.codex`. - -## Councils - -A council is a declarative multi-agent topology defined in a TOML file: named -agents plus routes that relay output between them on state transitions -(`ready`/`waiting`), with `join = "all"` fan-in, extraction modes, round limits, -and an optional `done_signal` for early termination. See -[`examples/council.toml`](examples/council.toml) for a fully commented -author/critic review loop. - -Launch one at startup with `--council ` or at runtime from the command -bar (`alt-c`): - -``` -council # spawn the agents and start routing -council status # current round / completion state -council stop # detach the router; sessions keep running -``` - -Council members are spawned with the minimal `SignalState` capability — they -can report their own state but cannot inject input, manage pipes, or create -sessions. Live progress (`round R/M`, done) is shown in the Status panel title. - -## Agent Chat - -Press `alt-t` for a chat pane that talks to everything linkshell manages — -council members, individual sessions, and configured local LLMs — without -switching panes: - -``` -@critic what did you find? address a session by name (or @2 by number) -@qwen summarize this diff address a local LLM from [agents.*] -@all status update please broadcast to every AI session -looks good, continue bare messages go to the last target -/new claude worker any command-bar command works with / -/yes /no answer a pending permission prompt -/agents list everyone you can talk to -``` - -Messages to sessions are injected into their PTY; when the session returns to -READY its answer is extracted (last code block, falling back to recent lines) -back into the transcript. Local LLM agents keep a bounded per-agent -conversation history. - -The transcript scrolls with the mouse wheel or `PageUp`/`PageDown` (a marker -on the input separator shows how far up you are). Drag to select transcript -text — it is copied to the clipboard on release, like the session panes. -Pasting into the chat input works too; multi-line pastes are delivered to -sessions via bracketed paste so they arrive as one message. - -When an AI session stops on a permission dialog or y/n question, the prompt -is posted into the chat transcript. `/yes` and `/no` answer the most recent -request with the CLI's own keys (claude: `1`/Esc, codex: `y`/`n`); use -`/yes ` or `/no ` to target a specific one, or type -anything else with `@name `. - -Local LLM agents are any OpenAI-compatible endpoint — llama.cpp server, -Ollama, vLLM, LM Studio: - -```toml -[agents.qwen] -endpoint = "http://localhost:8080/v1" # /v1 optional -model = "qwen3.6-27b" -system = "You are a concise coding assistant." -# api_key = "..." # sent as Bearer if set -``` - -**Orchestration pattern**: spawn a Claude session as your foreman, promote it -with `/grant 1 operator`, and delegate from chat — it can then use -`linkshell-ctl` to create sessions, inject prompts, wait for READY, and wire -pipes, while you stay in the chat pane. - -## Orchestrator Agent - -Linkshell can run a resident agent that keeps track of every session, chats -with you in the chat pane, and acts on your behalf — "start a claude session -in ~/proj and have it fix the parser bug" from chat, no keystrokes in any -session. It is also woken proactively when a session hits WAITING, ERROR, or -dies, and posts a short summary of what's blocked. - -```toml -[orchestrator] -enabled = true -provider = "anthropic" # anthropic | openai | lmstudio (API loop) - # claude | codex | opencode | omp (CLI session) -name = "agent" # chat target: @agent ... -# model = "claude-opus-4-8" -# endpoint = "http://localhost:1234/v1" # openai/lmstudio -# api_key = "..." # else ANTHROPIC_API_KEY / OPENAI_API_KEY -# system = "extra instructions" -# skills_dir = "~/.config/linkshell/skills" # *.md skill files; defaults to -# # this path when the dir exists -# memory_file = "~/.config/linkshell/memory.md" # persistent notes (default) -# hidden = true # CLI class: keep the agent out of the session bar -# permission_mode = "accept-edits" # CLI class: start with safe auto-approval -# # flags (claude: --permission-mode acceptEdits, -# # codex: --full-auto); "default" disables -# events = ["waiting", "error", "dead"] -# event_cooldown_secs = 30 -``` - -Two provider classes: - -- **API class** (`anthropic`, `openai`, `lmstudio`): an in-process tool-use - loop with tools for listing sessions, reading output, starting sessions - (with cwd + initial prompt), typing into sessions, and managing pipes. -- **CLI class** (`claude`, `codex`, `opencode`, `omp`): the CLI runs as a - session with operator-tier IPC capabilities and drives linkshell via - `linkshell-ctl` (`list`, `read`, `new`, `input --wait`, `pipe`, `chat`). By - default it is *hidden*: no session bar slot, no Alt+N digit, doesn't count - against the 8-session limit — you talk to it through the chat pane and it - replies through `linkshell-ctl chat`. Set `hidden = false` (or use - `:orchestrator show|hide` at runtime) to give it a visible session tab. - CLI-class orchestrators launch with `permission_mode = "accept-edits"` by - default — the CLI's own safe auto-approval flags — so routine edits don't - stop to ask. Bypass-style modes are rejected, same as - `--dangerously-skip-permissions` in session commands. - If the hidden CLI still hits a permission dialog or errors, the prompt is - posted to chat — answer it right there with `/yes` / `/no`, or type any - other reply with `@agent `; it is typed into its terminal. - -**Skills** give the orchestrator reusable playbooks. Drop `*.md` files into -`~/.config/linkshell/skills/` (or set `skills_dir`): the file stem is the -skill name, and the description comes from a `description:` line in leading -`---` frontmatter (or the first non-empty line). Only name + description go -into the prompt; the full text is loaded on demand — API-class orchestrators -call a `use_skill` tool, CLI-class orchestrators get the file paths in their -briefing and read them directly. - -**Memory** persists across restarts. The orchestrator carries a small notes -file — `~/.config/linkshell/memory.md` by default, or `memory_file` — that is -injected into its prompt each turn and appended to via a `remember` tool -(project layout, user preferences, recurring commands; one sentence per note). -You curate the file by hand; it is scaffolded automatically on first start. -See [docs/orchestrator-memory.md](docs/orchestrator-memory.md) for details. - -In chat, unaddressed messages default to the orchestrator when one is running. -`:orchestrator start|stop|restart|reset|pause|resume|status|show|hide` manages it -at runtime (also usable from chat as `/orchestrator …`). If the agent dies — its -task exits or the CLI session ends — a chat notice appears with the restart -command. `pause` keeps the orchestrator's context but drops incoming chat and -session events until `resume` (CLI-class orchestrators are also SIGSTOPped), -unlike `stop`, which discards its conversation. - -The orchestrator can never kill a session on its own: a kill request shows up -in chat and only `/confirm-kill` executes it (`/deny-kill` refuses). - -If an API-class orchestrator gets stuck mid-turn — spinning through tool -iterations or blocked waiting on a session — `/interrupt` (alias `/stop`) -breaks the turn at the next safe point. Blocked tool calls return -"interrupted by user" to the model, so its history stays coherent and it -can be redirected on the next message. - -`/reset` clears an API-class orchestrator's conversation context in place — -useful when the context has filled up with monitoring events — while keeping -the task and its token totals. If the agent task has died, `/reset` falls back -to a full restart, so it always leaves a working orchestrator behind. - -## Local agent sessions - -Sessions running `opencode`, `omp` (oh-my-pi), `pi`, `aider`, `llama-cli`, or -`ollama` are recognized as local agents: they get agent-style state inference -(THINKING on spinners/working verbs, READY on idle prompts) and terminal-based -token scraping. Wrappers with other names can be mapped with -`kind = "local"` in `[sessions.aliases]`. - -## Scrollback - -`alt-shift-PageUp/PageDown` (and `alt-shift-↑/↓`) scroll every session type -the same way. Shells use the terminal's native scrollback; full-screen TUIs -(claude, codex, opencode) scroll through linkshell's captured line history, -shown dimmed. The view holds position while new output streams in — typing -returns you to the live tail. - -## Keybindings - -| Key | Action | -|-----|--------| -| `alt-n` | New session dialog | -| `alt-c` | Open command bar | -| `alt-t` | Toggle agent chat pane | -| `alt-h` | Toggle help | -| `alt-x` | Kill active session | -| `alt-d` | Detach (sessions keep running) | -| `alt-\` | Split focused pane side by side | -| `alt--` | Split focused pane top/bottom | -| `alt-w` | Close focused pane (sibling reclaims the space) | -| `alt-r` | Rotate the focused pane's split direction | -| `alt-o` | Focus next pane | -| `alt-b` | Toggle broadcast input to all sessions | -| `alt-g` | Dock the chat pane | -| `alt-1` … `alt-8` | Switch to session by number | -| `alt-←` / `alt-→` | Cycle sessions | -| `ctrl-q` | Quit (shuts down the server and all sessions; use `alt-d` to leave them running) | -| `esc` | Dismiss overlay | -| `alt-shift-PageUp/PageDown` | Scroll output (page) | -| `alt-shift-↑` / `alt-shift-↓` | Scroll output (line) | - -All other input is passed through to the active session's PTY. - -## Command Bar - -Press `alt-c` to open. Available commands: - -``` -new claude [name] Start a Claude session -new codex [name] Start a Codex session -new shell [name] Start a shell session -new [name] Start a single-word command as a session -new custom Start a full command line (spaces, env prefixes) as a session -kill Kill the active session -kill Kill session by number -pause [n] Pause a session's process (SIGSTOP) — keeps its context, frees CPU -resume [n] Resume a paused session (SIGCONT) -council Launch a multi-agent council -council status Show council round / completion state -council stop Detach the council router (sessions keep running) -restart [n] Respawn a session with the same command, name, and cwd -profile save Save the current sessions and pipes as a startup profile -grant Set a session's IPC capabilities (operator|worker|council) -config path Show the config file location -config edit Open the config in $EDITOR (as a session) -config reload Re-read linkshell.toml without restarting -pipe [--extract=last-block|last-n=N|diff] [--summarize=N] [--on=ready|waiting|manual] [--prefix="..."] - Forward output from src to dst on state change -pipe fire [src] [dst] Manually fire a pipe with trigger=manual -unpipe [dst] Remove pipe(s) from src -pipes Inspect, pause, fire, or delete configured pipes -detach Detach the client; the server and sessions keep running -quit Exit linkshell (shuts down the server and all sessions) -``` - -## New Session Dialog - -Press `alt-n` for the interactive dialog. Use arrow keys or `1`–`4` to pick the session type, `tab` to move between fields, `enter` to create. - -## Session States - -| State | Meaning | Border | -|-------|---------|--------| -| STARTING | Process spawning | — | -| READY | Prompt detected, waiting for input | — | -| THINKING | AI model is processing | — | -| RUNNING | Active output streaming | — | -| WAITING | Agent asked you something, blocked | 🟡 yellow | -| ERROR | Error pattern detected or process crashed | 🔴 red flash | -| DEAD | Process exited | — | - -## Status Panel - -Each session gets one row: - -``` -1 🟠 →2 THINKING 1m 32s │ ~450 tok │ ~$0.02 -``` - -`→2` means this session has an active pipe to session 2. The arrow goes bold for one second when the pipe fires. Token counts and cost come from the JSONL logs written by Claude and Codex — not from screen scraping. Shell and custom sessions show `—`. - -## Pipes - -Pipes forward a snapshot of one session's output to another when a trigger fires. They are edge-triggered on state change, not continuous. - -``` -pipe 1 2 on READY, forward last code block -pipe 1 2 --on=waiting fire when session 1 hits WAITING -pipe 1 2 --extract=last-n=20 last 20 lines instead of last block -pipe 1 2 --extract=diff lines starting with + or - -pipe 1 2 --summarize=150 relay through Haiku first, max 150 tokens -pipe 1 2 --prefix="Review this:" prepend text to the forwarded content -pipe fire 1 2 manually fire a --on=manual pipe -unpipe 1 remove all pipes from session 1 -unpipe 1 2 remove the specific 1→2 pipe -``` - -## Agent Integration - -Every spawned session gets three environment variables set automatically: - -```bash -LINKSHELL_SESSION_ID=3 # this session's id -LINKSHELL_SOCK=/run/user/1000/linkshell/12345.sock -LINKSHELL_TOKEN= # capability token binding this session's rights -``` - -`linkshell-ctl` picks these up automatically (it presents the token in its -handshake, so a connection from inside a session carries that session's -capabilities — see below). Outside any session it falls back to the last -daemon socket recorded in `~/.config/linkshell/last_socket`, or `$LINKSHELL_SOCK`. - -```bash -linkshell-ctl list # JSON snapshot of all sessions (incl. cwd) -linkshell-ctl state READY # signal done; fires OnReady pipes -linkshell-ctl state THINKING # signal working -linkshell-ctl output "step done" # inject a line into this session's display -linkshell-ctl send [--wait] # direct-message another agent -linkshell-ctl wait-ready [--timeout=N] # block until session returns to READY -linkshell-ctl pipe list / add / remove / fire # manage pipes (operator capability) -linkshell-ctl new [name] [--cwd=PATH] # start a session (operator capability) -linkshell-ctl input [--wait] # type into a session; --wait returns its answer -linkshell-ctl read [n] # last n output lines of a session -linkshell-ctl chat # post a line into the chat pane -linkshell-ctl kill [reason...] # request a kill; the user must /confirm-kill -``` - -### Capabilities - -Every IPC connection is scoped by a capability set, resolved at handshake: - -| Tier | Who gets it | Can do | -|------|-------------|--------| -| operator | the human (same-uid Unix peer without a token), shell sessions, headless registrations | everything, incl. `session_create`, `session_input_wait`, pipe management | -| worker | spawned Claude/Codex/custom sessions (via `LINKSHELL_TOKEN`) | report state/tokens/output, query, direct-message, fire pipes | -| council | council members | report their own state only | -| orchestrator | the resident CLI-class orchestrator session | same as operator (incl. `chat_post` and `session_kill_request`; kills still require human `/confirm-kill`) | - -TCP connections must present a valid token; tokenless TCP is rejected. - -### Claude Code hooks - -Auto-signal state without changing your prompts: - -```json -{ - "hooks": { - "Stop": [{ "command": "linkshell-ctl state READY" }], - "PreToolUse": [{ "command": "linkshell-ctl state THINKING" }] - } -} -``` - -### Remote agents - -With `--tcp`, remote agents connect over the network using the same typed -JSONL protocol as the Unix socket. Every message travels in an envelope — -`{"msg": {...}}`, plus an `"id"` on requests that expect a reply — and every -connection starts with a `hello`/`welcome` handshake. TCP requires a token -(mint one by spawning the agent locally, or register headlessly over Unix -first); same-uid Unix connections without a token get operator rights. - -```python -import socket, json - -s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.connect(("host", 7373)) -f = s.makefile("rwb") - -def send(msg, req_id=None): - env = {"msg": msg} if req_id is None else {"id": req_id, "msg": msg} - f.write(json.dumps(env).encode() + b"\n"); f.flush() - -# Handshake — name registers a headless session slot (Unix); TCP needs token -send({"type": "hello", "protocol": 1, "token": TOKEN, "name": "remote-claude"}) -welcome = json.loads(f.readline())["msg"] # session_id, capabilities - -# Signal state -send({"type": "state", "state": "THINKING"}) - -# Synchronous query (note the id) -send({"type": "query", "what": "sessions"}, req_id=1) -sessions = json.loads(f.readline()) - -# Receive pipe relay content -env = json.loads(f.readline()) -if env["msg"]["type"] == "relay": - process(env["msg"]["content"]) -``` - -Message types: `hello`, `state`, `tokens`, `output`, `agent_send`, `broadcast`, -`fire_pipe`, `pipe_add`, `pipe_remove`, `session_create`, `session_input_wait`, -`query` — each gated by the connection's capabilities. Server→agent messages: -`welcome`, `relay`, `reply`, `error`. - ## Built With - [Ratatui](https://ratatui.rs) — TUI framework diff --git a/docs/agent-integration.md b/docs/agent-integration.md new file mode 100644 index 0000000..b539ae9 --- /dev/null +++ b/docs/agent-integration.md @@ -0,0 +1,108 @@ +# Agent Integration + +Sessions can drive linkshell and talk to each other over a typed IPC protocol. +Every connection is scoped by a capability set so agents get exactly the rights +they need — no more. + +- [linkshell-ctl](#linkshell-ctl) +- [Capabilities](#capabilities) +- [Claude Code hooks](#claude-code-hooks) +- [Remote agents](#remote-agents) + +## linkshell-ctl + +Every spawned session gets three environment variables set automatically: + +```bash +LINKSHELL_SESSION_ID=3 # this session's id +LINKSHELL_SOCK=/run/user/1000/linkshell/12345.sock +LINKSHELL_TOKEN= # capability token binding this session's rights +``` + +`linkshell-ctl` picks these up automatically (it presents the token in its +handshake, so a connection from inside a session carries that session's +capabilities). Outside any session it falls back to the last daemon socket +recorded in `~/.config/linkshell/last_socket`, or `$LINKSHELL_SOCK`. + +```bash +linkshell-ctl list # JSON snapshot of all sessions (incl. cwd) +linkshell-ctl state READY # signal done; fires OnReady pipes +linkshell-ctl state THINKING # signal working +linkshell-ctl output "step done" # inject a line into this session's display +linkshell-ctl send [--wait] # direct-message another agent +linkshell-ctl wait-ready [--timeout=N] # block until session returns to READY +linkshell-ctl pipe list / add / remove / fire # manage pipes (operator capability) +linkshell-ctl new [name] [--cwd=PATH] # start a session (operator capability) +linkshell-ctl input [--wait] # type into a session; --wait returns its answer +linkshell-ctl read [n] # last n output lines of a session +linkshell-ctl chat # post a line into the chat pane +linkshell-ctl kill [reason...] # request a kill; the user must /confirm-kill +``` + +## Capabilities + +Every IPC connection is scoped by a capability set, resolved at handshake: + +| Tier | Who gets it | Can do | +|------|-------------|--------| +| operator | the human (same-uid Unix peer without a token), shell sessions, headless registrations | everything, incl. `session_create`, `session_input_wait`, pipe management | +| worker | spawned Claude/Codex/custom sessions (via `LINKSHELL_TOKEN`) | report state/tokens/output, query, direct-message, fire pipes | +| council | council members | report their own state only | +| orchestrator | the resident CLI-class orchestrator session | same as operator (incl. `chat_post` and `session_kill_request`; kills still require human `/confirm-kill`) | + +TCP connections must present a valid token; tokenless TCP is rejected. + +## Claude Code hooks + +Auto-signal state without changing your prompts: + +```json +{ + "hooks": { + "Stop": [{ "command": "linkshell-ctl state READY" }], + "PreToolUse": [{ "command": "linkshell-ctl state THINKING" }] + } +} +``` + +## Remote agents + +With `--tcp`, remote agents connect over the network using the same typed JSONL +protocol as the Unix socket. Every message travels in an envelope — +`{"msg": {...}}`, plus an `"id"` on requests that expect a reply — and every +connection starts with a `hello`/`welcome` handshake. TCP requires a token +(mint one by spawning the agent locally, or register headlessly over Unix +first); same-uid Unix connections without a token get operator rights. + +```python +import socket, json + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect(("host", 7373)) +f = s.makefile("rwb") + +def send(msg, req_id=None): + env = {"msg": msg} if req_id is None else {"id": req_id, "msg": msg} + f.write(json.dumps(env).encode() + b"\n"); f.flush() + +# Handshake — name registers a headless session slot (Unix); TCP needs token +send({"type": "hello", "protocol": 1, "token": TOKEN, "name": "remote-claude"}) +welcome = json.loads(f.readline())["msg"] # session_id, capabilities + +# Signal state +send({"type": "state", "state": "THINKING"}) + +# Synchronous query (note the id) +send({"type": "query", "what": "sessions"}, req_id=1) +sessions = json.loads(f.readline()) + +# Receive pipe relay content +env = json.loads(f.readline()) +if env["msg"]["type"] == "relay": + process(env["msg"]["content"]) +``` + +Message types: `hello`, `state`, `tokens`, `output`, `agent_send`, `broadcast`, +`fire_pipe`, `pipe_add`, `pipe_remove`, `session_create`, `session_input_wait`, +`query` — each gated by the connection's capabilities. Server→agent messages: +`welcome`, `relay`, `reply`, `error`. diff --git a/docs/chat.md b/docs/chat.md new file mode 100644 index 0000000..a485c61 --- /dev/null +++ b/docs/chat.md @@ -0,0 +1,55 @@ +# Agent Chat + +Press `alt-t` for a chat pane that talks to everything linkshell manages — +council members, individual sessions, and configured local LLMs — without +switching panes: + +``` +@critic what did you find? address a session by name (or @2 by number) +@qwen summarize this diff address a local LLM from [agents.*] +@all status update please broadcast to every AI session +looks good, continue bare messages go to the last target +/new claude worker any command-bar command works with / +/yes /no answer a pending permission prompt +/agents list everyone you can talk to +``` + +Messages to sessions are injected into their PTY; when the session returns to +READY its answer is extracted (last code block, falling back to recent lines) +back into the transcript. Local LLM agents keep a bounded per-agent conversation +history. + +The transcript scrolls with the mouse wheel or `PageUp`/`PageDown` (a marker on +the input separator shows how far up you are). Drag to select transcript text — +it is copied to the clipboard on release, like the session panes. Pasting into +the chat input works too; multi-line pastes are delivered to sessions via +bracketed paste so they arrive as one message. Dock the pane with `alt-g`. + +## Answering permission prompts + +When an AI session stops on a permission dialog or y/n question, the prompt is +posted into the chat transcript. `/yes` and `/no` answer the most recent request +with the CLI's own keys (claude: `1`/Esc, codex: `y`/`n`); use `/yes ` +or `/no ` to target a specific one, or type anything else with +`@name `. + +## Local LLM agents + +Local LLM agents are any OpenAI-compatible endpoint — llama.cpp server, Ollama, +vLLM, LM Studio: + +```toml +[agents.qwen] +endpoint = "http://localhost:8080/v1" # /v1 optional +model = "qwen3.6-27b" +system = "You are a concise coding assistant." +# api_key = "..." # sent as Bearer if set +``` + +## Orchestration pattern + +Spawn a Claude session as your foreman, promote it with `/grant 1 operator`, and +delegate from chat — it can then use `linkshell-ctl` to create sessions, inject +prompts, wait for READY, and wire pipes, while you stay in the chat pane. For a +resident agent that does this automatically, see the +[orchestrator](orchestrator.md). diff --git a/docs/councils.md b/docs/councils.md new file mode 100644 index 0000000..2262238 --- /dev/null +++ b/docs/councils.md @@ -0,0 +1,21 @@ +# Councils + +A council is a declarative multi-agent topology defined in a TOML file: named +agents plus routes that relay output between them on state transitions +(`ready`/`waiting`), with `join = "all"` fan-in, extraction modes, round limits, +and an optional `done_signal` for early termination. See +[`examples/council.toml`](../examples/council.toml) for a fully commented +author/critic review loop. + +Launch one at startup with `--council ` or at runtime from the command bar +(`alt-c`): + +``` +council # spawn the agents and start routing +council status # current round / completion state +council stop # detach the router; sessions keep running +``` + +Council members are spawned with the minimal `SignalState` capability — they can +report their own state but cannot inject input, manage pipes, or create +sessions. Live progress (`round R/M`, done) is shown in the Status panel title. diff --git a/docs/orchestrator.md b/docs/orchestrator.md new file mode 100644 index 0000000..399ed4d --- /dev/null +++ b/docs/orchestrator.md @@ -0,0 +1,90 @@ +# Orchestrator Agent + +Linkshell can run a resident agent that keeps track of every session, chats with +you in the chat pane, and acts on your behalf — "start a claude session in ~/proj +and have it fix the parser bug" from chat, no keystrokes in any session. It is +also woken proactively when a session hits WAITING, ERROR, or dies, and posts a +short summary of what's blocked. + +```toml +[orchestrator] +enabled = true +provider = "anthropic" # anthropic | openai | lmstudio (API loop) + # claude | codex | opencode | omp (CLI session) +name = "agent" # chat target: @agent ... +# model = "claude-opus-4-8" +# endpoint = "http://localhost:1234/v1" # openai/lmstudio +# api_key = "..." # else ANTHROPIC_API_KEY / OPENAI_API_KEY +# system = "extra instructions" +# skills_dir = "~/.config/linkshell/skills" # *.md skill files; defaults to +# # this path when the dir exists +# memory_file = "~/.config/linkshell/memory.md" # persistent notes (default) +# hidden = true # CLI class: keep the agent out of the session bar +# permission_mode = "accept-edits" # CLI class: start with safe auto-approval +# # flags (claude: --permission-mode acceptEdits, +# # codex: --full-auto); "default" disables +# events = ["waiting", "error", "dead"] +# event_cooldown_secs = 30 +``` + +## Provider classes + +- **API class** (`anthropic`, `openai`, `lmstudio`): an in-process tool-use loop + with tools for listing sessions, reading output, starting sessions (with cwd + + initial prompt), typing into sessions, and managing pipes. +- **CLI class** (`claude`, `codex`, `opencode`, `omp`): the CLI runs as a session + with operator-tier IPC capabilities and drives linkshell via `linkshell-ctl` + (`list`, `read`, `new`, `input --wait`, `pipe`, `chat`). By default it is + *hidden*: no session bar slot, no Alt+N digit, doesn't count against the + 8-session limit — you talk to it through the chat pane and it replies through + `linkshell-ctl chat`. Set `hidden = false` (or use `:orchestrator show|hide` at + runtime) to give it a visible session tab. CLI-class orchestrators launch with + `permission_mode = "accept-edits"` by default — the CLI's own safe + auto-approval flags — so routine edits don't stop to ask. Bypass-style modes + are rejected, same as `--dangerously-skip-permissions` in session commands. If + the hidden CLI still hits a permission dialog or errors, the prompt is posted + to chat — answer it right there with `/yes` / `/no`, or type any other reply + with `@agent `; it is typed into its terminal. + +## Skills + +Skills give the orchestrator reusable playbooks. Drop `*.md` files into +`~/.config/linkshell/skills/` (or set `skills_dir`): the file stem is the skill +name, and the description comes from a `description:` line in leading `---` +frontmatter (or the first non-empty line). Only name + description go into the +prompt; the full text is loaded on demand — API-class orchestrators call a +`use_skill` tool, CLI-class orchestrators get the file paths in their briefing +and read them directly. + +## Memory + +Memory persists across restarts. The orchestrator carries a small notes file — +`~/.config/linkshell/memory.md` by default, or `memory_file` — that is injected +into its prompt each turn and appended to via a `remember` tool (project layout, +user preferences, recurring commands; one sentence per note). You curate the file +by hand; it is scaffolded automatically on first start. See +[docs/orchestrator-memory.md](orchestrator-memory.md) for details. + +## Runtime control + +In chat, unaddressed messages default to the orchestrator when one is running. +`:orchestrator start|stop|restart|reset|pause|resume|status|show|hide` manages it +at runtime (also usable from chat as `/orchestrator …`). If the agent dies — its +task exits or the CLI session ends — a chat notice appears with the restart +command. `pause` keeps the orchestrator's context but drops incoming chat and +session events until `resume` (CLI-class orchestrators are also SIGSTOPped), +unlike `stop`, which discards its conversation. + +The orchestrator can never kill a session on its own: a kill request shows up in +chat and only `/confirm-kill` executes it (`/deny-kill` refuses). + +If an API-class orchestrator gets stuck mid-turn — spinning through tool +iterations or blocked waiting on a session — `/interrupt` (alias `/stop`) breaks +the turn at the next safe point. Blocked tool calls return "interrupted by user" +to the model, so its history stays coherent and it can be redirected on the next +message. + +`/reset` clears an API-class orchestrator's conversation context in place — +useful when the context has filled up with monitoring events — while keeping the +task and its token totals. If the agent task has died, `/reset` falls back to a +full restart, so it always leaves a working orchestrator behind. diff --git a/docs/panes-and-navigation.md b/docs/panes-and-navigation.md new file mode 100644 index 0000000..79f3833 --- /dev/null +++ b/docs/panes-and-navigation.md @@ -0,0 +1,104 @@ +# Panes, Navigation & Keybindings + +- [Split panes](#split-panes) +- [Scrollback](#scrollback) +- [Status panel](#status-panel) +- [Keybindings](#keybindings) +- [Command bar](#command-bar) + +## Split panes + +Split any pane side by side (`alt-\`) or top/bottom (`alt--`), repeatedly and in +any direction, for arbitrary tiled layouts. The session bar is centered and +reflows based on how many sessions are open. + +| Key | Action | +|-----|--------| +| `alt-\` | Split focused pane side by side | +| `alt--` | Split focused pane top/bottom | +| `alt-w` | Close focused pane (sibling reclaims the space) | +| `alt-r` | Rotate the focused pane's split direction | +| `alt-o` | Focus next pane | + +## Scrollback + +`alt-shift-PageUp/PageDown` (and `alt-shift-↑/↓`) scroll every session type the +same way. Shells use the terminal's native scrollback; full-screen TUIs (claude, +codex, opencode) scroll through linkshell's captured line history, shown dimmed. +The view holds position while new output streams in — typing returns you to the +live tail. + +Mouse text selection works everywhere: drag to select, auto-copies to clipboard. + +## Status panel + +Each session gets one row: + +``` +1 🟠 →2 THINKING 1m 32s │ ~450 tok │ ~$0.02 +``` + +`→2` means this session has an active pipe to session 2. The arrow goes bold for +one second when the pipe fires. Token counts and cost come from the JSONL logs +written by Claude and Codex — not from screen scraping. Shell and custom +sessions show `—`. On Pro/Max subscriptions, linkshell detects the subscription +and shows real token counts while skipping meaningless cost. + +## Keybindings + +| Key | Action | +|-----|--------| +| `alt-n` | New session dialog | +| `alt-c` | Open command bar | +| `alt-t` | Toggle agent chat pane | +| `alt-h` | Toggle help | +| `alt-x` | Kill active session | +| `alt-d` | Detach (sessions keep running) | +| `alt-\` | Split focused pane side by side | +| `alt--` | Split focused pane top/bottom | +| `alt-w` | Close focused pane (sibling reclaims the space) | +| `alt-r` | Rotate the focused pane's split direction | +| `alt-o` | Focus next pane | +| `alt-b` | Toggle broadcast input to all sessions | +| `alt-g` | Dock the chat pane | +| `alt-1` … `alt-8` | Switch to session by number | +| `alt-←` / `alt-→` | Cycle sessions | +| `ctrl-q` | Quit (shuts down the server and all sessions; use `alt-d` to leave them running) | +| `esc` | Dismiss overlay | +| `alt-shift-PageUp/PageDown` | Scroll output (page) | +| `alt-shift-↑` / `alt-shift-↓` | Scroll output (line) | + +All other input is passed through to the active session's PTY. Keybindings are +configurable — see the [configuration reference](config-reference.md). + +## Command bar + +Press `alt-c` to open. Available commands: + +``` +new claude [name] Start a Claude session +new codex [name] Start a Codex session +new shell [name] Start a shell session +new [name] Start a single-word command as a session +new custom Start a full command line (spaces, env prefixes) as a session +kill Kill the active session +kill Kill session by number +pause [n] Pause a session's process (SIGSTOP) — keeps its context, frees CPU +resume [n] Resume a paused session (SIGCONT) +council Launch a multi-agent council +council status Show council round / completion state +council stop Detach the council router (sessions keep running) +restart [n] Respawn a session with the same command, name, and cwd +profile save Save the current sessions and pipes as a startup profile +grant Set a session's IPC capabilities (operator|worker|council) +config path Show the config file location +config edit Open the config in $EDITOR (as a session) +config reload Re-read linkshell.toml without restarting +pipe [--extract=last-block|last-n=N|diff] [--summarize=N] [--on=ready|waiting|manual] [--prefix="..."] + Forward output from src to dst on state change +pipe fire [src] [dst] Manually fire a pipe with trigger=manual +unpipe [dst] Remove pipe(s) from src +pipes Inspect, pause, fire, or delete configured pipes +detach Detach the client; the server and sessions keep running +quit Exit linkshell (shuts down the server and all sessions) +``` diff --git a/docs/pipes.md b/docs/pipes.md new file mode 100644 index 0000000..00ecb3b --- /dev/null +++ b/docs/pipes.md @@ -0,0 +1,23 @@ +# Pipes + +Pipes forward a snapshot of one session's output to another when a trigger +fires. They are **edge-triggered on state change, not continuous** — when a +source session hits a trigger state (`OnReady`, `OnWaiting`, or `Manual`), +linkshell extracts a snapshot and forwards it to the destination's PTY. + +``` +pipe 1 2 on READY, forward last code block +pipe 1 2 --on=waiting fire when session 1 hits WAITING +pipe 1 2 --extract=last-n=20 last 20 lines instead of last block +pipe 1 2 --extract=diff lines starting with + or - +pipe 1 2 --summarize=150 relay through Haiku first, max 150 tokens +pipe 1 2 --prefix="Review this:" prepend text to the forwarded content +pipe fire 1 2 manually fire a --on=manual pipe +unpipe 1 remove all pipes from session 1 +unpipe 1 2 remove the specific 1→2 pipe +pipes inspect, pause, fire, or delete configured pipes +``` + +Active pipes show as `→ N` in the status panel, bold for one tick when they +fire. The `--summarize` mode relays through Haiku (`claude-haiku-4-5-20251001`) +to compress the snapshot first. diff --git a/docs/sessions.md b/docs/sessions.md new file mode 100644 index 0000000..e35ef76 --- /dev/null +++ b/docs/sessions.md @@ -0,0 +1,128 @@ +# Sessions + +Linkshell runs up to 8 concurrent PTY sessions — Claude, Codex, local agents, +shells, or any custom command — as a tmux-style client/server pair. The server +owns the sessions and survives detach; the foreground TUI is just a client. + +- [Starting & managing sessions](#starting--managing-sessions) +- [Detach, reattach & multiple linkshells](#detach-reattach--multiple-linkshells) +- [Startup profiles](#startup-profiles) +- [Aliased Claude / Codex sessions](#aliased-claude--codex-sessions) +- [Local agent sessions](#local-agent-sessions) +- [Session states](#session-states) + +## Starting & managing sessions + +Create your first session with `alt-n` (interactive dialog) or from the command +bar (`alt-c`): + +``` +new claude [name] Start a Claude session +new codex [name] Start a Codex session +new shell [name] Start a shell session +new [name] Start a single-word command as a session +new custom Start a full command line (spaces, env prefixes) as a session +kill [n] Kill the active session (or session n) +pause [n] Pause a session's process (SIGSTOP) — keeps context, frees CPU +resume [n] Resume a paused session (SIGCONT) +restart [n] Respawn a session with the same command, name, and cwd +``` + +In the `alt-n` dialog, use arrow keys or `1`–`4` to pick the session type, +`tab` to move between fields, `enter` to create. + +Sessions are color-coded by type: 🟠 orange for Claude, 🔵 blue for Codex. + +## Detach, reattach & multiple linkshells + +Linkshell runs as a client/server pair, like tmux/screen: each `linkshell` +starts a background server that owns its sessions, and the foreground TUI is a +client attached to it. `alt-d` detaches — sessions keep running. + +You can run **multiple independent linkshells** on one machine, screen-style: + +```bash +linkshell # start a new detached server and attach +linkshell new work # start a new one named "work" +linkshell ls # list detached sessions (id, name, pid, status) +linkshell -r # reattach to a specific session by id +linkshell -r # reattach when exactly one session is running +``` + +Each server has its own id, pid, and sockets; `linkshell ls` prunes any whose +process has died. + +If something looks wrong (missing logs, stale socket, nested multiplexer, +limited terminal colors), run `linkshell doctor` for a diagnostic report. + +## Startup profiles + +Save a layout of sessions and pipes and relaunch it later: + +``` +profile save # from the command bar +linkshell --profile # relaunch it at startup +``` + +## Aliased Claude / Codex sessions + +Sessions running Claude or Codex under a different config home are recognized +and get the full treatment — state inference patterns, the JSONL token/cost +watcher, and pricing — instead of being treated as generic custom commands. + +Two ways to spell them: + +**Inline env prefix** — just works, no config needed. The classifier sees +through leading `VAR=value` assignments, and the watcher reads the config home +from the command itself: + +``` +new custom CLAUDE_CONFIG_DIR=~/.claude-work claude +new custom CODEX_HOME=~/.codex-personal codex +``` + +(or enter the same command line in the `alt-n` dialog's Custom field) + +**Config alias** — for wrapper scripts or shell aliases whose name doesn't +contain `claude`/`codex`. Map the command basename in `[sessions.aliases]`: + +```toml +[sessions.aliases.claude-work] +kind = "claude" +config_dir = "~/.claude-work" # exported as CLAUDE_CONFIG_DIR + +[sessions.aliases.cx] +kind = "codex" +config_dir = "~/.codex-personal" # exported as CODEX_HOME +``` + +When `config_dir` comes from an alias it is also injected into the session's +environment, so the CLI and the log watcher agree on where the config home is. +Precedence for the watcher: inline env prefix → alias `config_dir` → +`$CLAUDE_CONFIG_DIR`/`$CODEX_HOME` in linkshell's own environment → the default +`~/.claude` / `~/.codex`. + +## Local agent sessions + +Sessions running `opencode`, `omp` (oh-my-pi), `pi`, `aider`, `llama-cli`, or +`ollama` are recognized as local agents: they get agent-style state inference +(THINKING on spinners/working verbs, READY on idle prompts) and terminal-based +token scraping. Wrappers with other names can be mapped with `kind = "local"` +in `[sessions.aliases]`. + +## Session states + +States are inferred from PTY output and refined by JSONL log activity. + +| State | Meaning | Border | +|-------|---------|--------| +| STARTING | Process spawning | — | +| READY | Prompt detected, waiting for input | — | +| THINKING | AI model is processing | — | +| RUNNING | Active output streaming | — | +| WAITING | Agent asked you something, blocked | 🟡 yellow | +| ERROR | Error pattern detected or process crashed | 🔴 red flash | +| DEAD | Process exited | — | + +Desktop notifications for WAITING/ERROR are configurable via notify-send, OSC 9, +or bell. See the [configuration reference](config-reference.md).