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
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
525 changes: 62 additions & 463 deletions README.md

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions docs/agent-integration.md
Original file line number Diff line number Diff line change
@@ -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=<hex> # 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] <name> <msg...> # direct-message another agent
linkshell-ctl wait-ready <id> [--timeout=N] # block until session <id> returns to READY
linkshell-ctl pipe list / add / remove / fire # manage pipes (operator capability)
linkshell-ctl new <kind> [name] [--cwd=PATH] # start a session (operator capability)
linkshell-ctl input <id> <text...> [--wait] # type into a session; --wait returns its answer
linkshell-ctl read <id> [n] # last n output lines of a session
linkshell-ctl chat <msg...> # post a line into the chat pane
linkshell-ctl kill <id> [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`.
55 changes: 55 additions & 0 deletions docs/chat.md
Original file line number Diff line number Diff line change
@@ -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 <session>`
or `/no <session>` to target a specific one, or type anything else with
`@name <text>`.

## 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).
21 changes: 21 additions & 0 deletions docs/councils.md
Original file line number Diff line number Diff line change
@@ -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 <file>` or at runtime from the command bar
(`alt-c`):

```
council <file.toml> # 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.
90 changes: 90 additions & 0 deletions docs/orchestrator.md
Original file line number Diff line number Diff line change
@@ -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 <text>`; 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.
Loading
Loading