detached 880bc67

refactor(subagents): canonicalize runtime names

commit
880bc67f2cbea83c2d31da95157300b39a13b978
author
brice · · 2026-08-05 02:36 UTC
committer
brice · · 2026-08-05 04:28 UTC
parent
3463e9c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
68 files · +1381 -1893
M AGENTS.md +1 -1
@@ -42,7 +42,7 @@ Put behavior in the package that owns the concern:
4242 | `packages/agent/extproto` | Extension wire protocol |
4343 | `packages/agent/ext` | Public extension SDK |
4444 | `packages/agent/skills` | Skill discovery and loading |
45-| `packages/agent/swarm` | Background-agent state, supervision, and persistence |
45+| `packages/agent/subagents` | Background-agent state, supervision, and persistence |
4646 | `packages/agent/tools` | Built-in tools, permission checks, and jail behavior |
4747 | `packages/tui` | Terminal input, layout, rendering, markdown, themes, and images |
4848 | `packages/ignore` | Ignore-pattern matching |
M CONTRIBUTING.md +1 -1
@@ -46,7 +46,7 @@ The major ownership boundaries are:
4646 - `packages/agent/modes`: interactive and non-interactive user modes
4747 - `packages/agent/extensions`, `extproto`, and `ext`: extension hosting, protocol, and SDK
4848 - `packages/agent/skills`: reusable instruction discovery
49-- `packages/agent/swarm`: background-agent supervision and state
49+- `packages/agent/subagents`: background-agent supervision and state
5050 - `packages/agent/tools`: built-in tools, permissions, and jail checks
5151 - `packages/tui`: terminal mechanics and rendering
5252 - `packages/ignore`: ignore matching
M README.md +9 -11
@@ -290,7 +290,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
290290 | `/session` | Four ops on the current session: `export` to a portable `.zotsession` file, `import` one back in, `fork` from a past user message into a new branch, `tree` to switch between branches. Opens a picker without an argument; direct forms: `/session export [path]`, `/session import <path>`, `/session fork`, `/session tree`. Default export destination is `~/Downloads`. `Esc` `Esc` from an idle, empty editor is a shortcut for `/session tree` when the terminal supplies two parsed bare Escape events. |
291291 | `/jump` | Scroll the chat to a previous turn (or `/jump <text>` to filter). |
292292 | `/btw` | Side chat with full context that doesn't add to the main thread. |
293-| `/subagents` | Spawn, monitor, and chat with background subagents. Each is a separate long-lived process with bounded lifecycle state, structured results, and optional Git worktree isolation. `/swarm` remains a compatibility alias. Named profiles can be selected with `agent`; see [docs/subagents.md](docs/subagents.md). |
293+| `/subagents` | Spawn, monitor, and chat with background subagents. Each is a separate long-lived process with bounded lifecycle state, structured results, and optional Git worktree isolation. Named profiles can be selected with `agent`; see [docs/subagents.md](docs/subagents.md). |
294294 | `/skills` | List discovered skills (SKILL.md files) and preview their bodies. |
295295 | `/compact` | Summarize the transcript into one message to free up context. |
296296 | `/study` | Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. Pass a path — typed, drag-dropped, or selected via `@` — to target a specific file or directory instead: `/study [dir:packages/]`, `/study cmd/zot/main.go`. |
@@ -298,7 +298,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
298298 | `/unjail` | Allow tools to touch paths outside again. |
299299 | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
300300 | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. |
301-| `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-swarm, fast mode, main/sub-agent LSP access, and the auto-compact threshold. Saved to `$ZOT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. |
301+| `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-subagents, fast mode, main/sub-agent LSP access, and the auto-compact threshold. Saved to `$ZOT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. |
302302 | `/clear` | Clear the chat transcript. |
303303 | `/exit` | Exit zot. |
304304
@@ -318,7 +318,7 @@ Four ops on the current session. `/session` alone opens a picker; each is also r
318318
319319 - **`/session export [path]`**. Writes the running transcript to a portable `.zotsession` file. Default destination is `~/Downloads/<timestamp>-<session-id>-<prompt-slug>.zotsession`. Pass a path to override; a directory is fine (a dated name is built inside), a bare name gets `.zotsession` appended. The meta's cwd is stripped on the way out so the recipient doesn't see your filesystem layout.
320320
321- **What's included.** Only the main chat thread of the running session — messages, tool calls, tool results, compactions, and usage. **`/swarm` subagents are NOT included.** Their transcripts, unix-socket inboxes, and per-agent session files are all machine-local; a `.zotsession` is just a chat transcript and has no way to revive a unix socket on another box. If you want the conversation, copy it out of the dashboard manually.
321+ **What's included.** Only the main chat thread of the running session — messages, tool calls, tool results, compactions, and usage. **`/subagents` subagents are NOT included.** Their transcripts, unix-socket inboxes, and per-agent session files are all machine-local; a `.zotsession` is just a chat transcript and has no way to revive a unix socket on another box. If you want the conversation, copy it out of the dashboard manually.
322322 - **`/session import <path>`**. Copies a `.zotsession` file into `$ZOT_HOME/sessions/<cwd-hash>/` with a fresh id and the current cwd, then switches the running agent onto it. Imported sessions are first-class: they show up in `/sessions` and `/jump`, and in the current-family tree when they are part of that family. Drag-drop paths in the editor are accepted (zot strips the surrounding quotes automatically).
323323 - **`/fork`** (also available as **`/session fork`**). Opens a turn picker (same shape as `/jump`). Pick any past user message; zot copies the transcript through that turn into a new branch and switches to it without starting a provider request. The parent session stays on disk. Use it to try a different question without polluting the original transcript, or to rewind after the agent went down the wrong path. Use a user-message row in `/session tree` when you want the prompt restored for editing, including image attachments.
324324 - **`/session tree`**. Shows the current session's family: its root ancestor and descendants, not unrelated root sessions in the same cwd. Use `up`/`down` and `enter` to choose a row; `esc` closes the tree. The picker keeps pre-compaction history available as forkable user and assistant messages, while hiding tool calls, tool results, compaction checkpoints, shell/image helper rows, and other internal context. Message rows are indented at their branch points and the current endpoint is tagged `[current]`. Empty and detached branch points are represented by visible boundary rows. A child whose historical fork point no longer matches after compaction stays discoverable as a detached branch under the parent's current tail rather than disappearing. Parentless sessions are roots; orphaned children (whose parent file was deleted) remain roots.
@@ -364,7 +364,6 @@ Background subagents that run alongside your main session. Each one is a separat
364364 /subagents restart-task <id> # intentionally run the stored task again
365365 /subagents kill <id> # cancel gracefully, then force-stop
366366 /subagents remove <id> # delete durable state
367-# /swarm remains an alias for /subagents
368367 ```
369368
370369 **Dashboard (`/subagents` with no arg)** — a list of every agent for the current session, with process/turn state, queue position, age, workspace mode, and current activity. Keys:
@@ -386,17 +385,17 @@ Background subagents that run alongside your main session. Each one is a separat
386385
387386 **Session scoping** — each agent is stamped with the host session that spawned it and only shows up in that session's dashboard. Swap sessions with `/sessions` and the dashboard re-narrows accordingly. Agents from other sessions keep running in the background and reappear when you switch back.
388387
389-**Persistence across zot restarts** — every spawn writes a durable manifest, append-only event log, session file, and structured `result.json` under `$ZOT_HOME/swarm/agents/<id>/`. On the next `zot` launch they show up as **detached**; use `R` or `/subagents resume-session <id>` to continue the existing session. `/subagents restart-task <id>` is the explicit operation that intentionally replays the stored task. Existing `$ZOT_HOME/swarm` state remains readable.
388+**Persistence across zot restarts** — every spawn writes a durable manifest, append-only event log, session file, and structured `result.json` under `$ZOT_HOME/subagents/agents/<id>/`. On the next `zot` launch they show up as **detached**; use `R` or `/subagents resume-session <id>` to continue the existing session. `/subagents restart-task <id>` is the explicit operation that intentionally replays the stored task.
390389
391-**Where state lives** — per-agent manifests, session files, events, results, and patches live under `$ZOT_HOME/swarm/agents/<id>/`; inbox sockets are runtime-only and permission-restricted. Shared-mode edits land directly in the repo. Worktree-mode edits are captured as durable patches and changed-file summaries before cleanup.
390+**Where state lives** — per-agent manifests, session files, events, results, and patches live under `$ZOT_HOME/subagents/agents/<id>/`; inbox sockets are runtime-only and permission-restricted. Shared-mode edits land directly in the repo. Worktree-mode edits are captured as durable patches and changed-file summaries before cleanup.
392391
393392 **`/session export` does NOT bundle subagents.** A `.zotsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually.
394393
395-**Auto-swarm.** With `/settings` -> auto-swarm on, the main agent gets the canonical `subagent_spawn` tool (`swarm_spawn` remains an alias) and a system-prompt nudge to delegate independent work. The tool returns the child id and lifecycle state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and `isolation:"shared"` or `isolation:"worktree"`. Global concurrency and total-spawn budgets apply equally to slash commands, tool calls, and batches. Off by default; toggle from `/settings`.
394+**Auto-subagents.** With `/settings` -> auto-subagents on, the main agent gets the canonical `subagent_spawn` tool and a system-prompt nudge to delegate independent work. The tool returns the child id and lifecycle state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and `isolation:"shared"` or `isolation:"worktree"`. Global concurrency and total-spawn budgets apply equally to slash commands, tool calls, and batches. Off by default; toggle from `/settings`.
396395
397396 ### Named subagent profiles
398397
399-zot discovers the common markdown/frontmatter profile layout from `~/.agents/agents/*.md` by default. With auto-swarm enabled, the main agent receives `[subagents_list]` metadata and can select a named profile through `subagent_spawn` (`swarm_spawn` remains an alias). Profile bodies are applied only to the selected child. See [docs/subagents.md](docs/subagents.md) for discovery, supported fields, reasoning levels, and examples.
398+zot discovers the common markdown/frontmatter profile layout from `~/.agents/agents/*.md` by default. With auto-subagents enabled, the main agent receives `[subagents_list]` metadata and can select a named profile through `subagent_spawn`. Profile bodies are applied only to the selected child. See [docs/subagents.md](docs/subagents.md) for discovery, supported fields, reasoning levels, and examples.
400399
401400 ### `/settings`
402401
@@ -405,7 +404,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
405404 - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol.
406405 - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all.
407406 - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zot: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`.
408-- **auto-swarm** — let the main agent spawn background sub-agents in parallel via the canonical `subagent_spawn` tool (`swarm_spawn` remains an alias). Off by default. The tool accepts named profiles, reasoning, timeout, turn limits, and shared/worktree isolation; lifecycle and result references are persisted. Completion updates still arrive through the auto-swarm watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.
407+- **auto-subagents** — let the main agent spawn background sub-agents in parallel via the canonical `subagent_spawn` tool. Off by default. The tool accepts named profiles, reasoning, timeout, turn limits, and shared/worktree isolation; lifecycle and result references are persisted. Completion updates still arrive through the auto-subagents watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.
409408 - **fast mode** — request OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; changes apply on the next model call and persist as `fast_mode`. Other providers return an unsupported-provider error. Fast mode may cost more and depends on the selected OpenAI model/account.
410409 - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`.
411410 - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts.
@@ -1068,8 +1067,7 @@ packages/agent/modes/bot/ protocol-agnostic bot runner (BotAdapter i
10681067 packages/agent/modes/telegram/ telegram adapter, api client, daemon
10691068 packages/agent/tools/ read, write, edit, bash, lsp, sandbox
10701069 packages/agent/skills/ skill discovery, frontmatter parser, skill tool
1071-packages/agent/subagents/ named markdown profile discovery and dispatch
1072-packages/agent/swarm/ background subagent runtime
1070+packages/agent/subagents/ named profiles, supervisor, and background runtime
10731071 packages/agent/sdk/ public Go SDK for embedding zot in-process (package sdk)
10741072 packages/agent/ext/ public Go SDK for writing extensions (package ext)
10751073 ```
M docs/lsp.md +1 -1
@@ -120,7 +120,7 @@ diagnostics allows a later recurrence to be surfaced again.
120120
121121 - **lsp in main session** (`lsp_enabled`) controls the main agent and applies
122122 immediately to the live tool registry.
123-- **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned swarm
123+- **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned subagent
124124 child processes.
125125
126126 The sub-agent setting does not grant access to a child whose profile or
M docs/providers.md +1 -1
@@ -123,7 +123,7 @@ off by default and is stored as `fast_mode` in `$ZOT_HOME/config.json`.
123123
124124 Fast mode is currently limited to those OpenAI provider IDs. Enabling it while
125125 using another provider returns an error instead of silently changing that
126-provider's request. Swarm children inherit the setting from their parent.
126+provider's request. Subagent children inherit the setting from their parent.
127127
128128 Example:
129129
M docs/subagents.md +3 -3
@@ -39,7 +39,7 @@ Supported metadata:
3939
4040 | Field | Meaning |
4141 |---|---|
42-| `name` | Name passed to `subagent_spawn`'s `agent` field (`swarm_spawn` is a compatibility alias). Falls back to the filename. |
42+| `name` | Name passed to `subagent_spawn`'s `agent` field. Falls back to the filename. |
4343 | `description` | Short description shown to the main agent. |
4444 | `tools` | Comma-separated or list-form tool names. zot enforces its built-in `read`, `write`, `edit`, `bash`, and `lsp` registry; the conditional `skill` tool is available when skills are enabled. Unknown names do not grant capabilities. |
4545 | `model` | Optional model ID. A qualified value such as `openai-codex/gpt-5.6-luna` selects both provider and model. |
@@ -54,7 +54,7 @@ Other frontmatter from another agent host is ignored when zot does not have an e
5454
5555 ## Selecting a profile
5656
57-When **auto-swarm** is enabled in `/settings`, the main agent receives a compact `[subagents_list]` section in its system prompt and can select a profile for `subagent_spawn`:
57+When **auto-subagents** is enabled in `/settings`, the main agent receives a compact `[subagents_list]` section in its system prompt and can select a profile for `subagent_spawn`:
5858
5959 ```json
6060 {
@@ -90,7 +90,7 @@ Shared mode preserves the historical host working directory. For parallel coding
9090
9191 Every child has independent process and turn state. A process may be `alive` while its turn is `idle`; a supervisor restart marks the process `detached` without claiming that its last turn failed. Durable manifests include the task, parent/root session identity, workspace mode, attempt, process/turn state, heartbeat timestamps, and logical result references.
9292
93-The worker and supervisor communicate over newline-delimited JSON. Current messages use version `1` envelopes with `version`, `message_id`, `agent_id`, `turn_id`, `timestamp`, and `payload`. Unknown event names and payload fields are retained. Older flat JSON events and text commands (`user ...`, `cancel`, `shutdown`) remain readable during the compatibility window.
93+The worker and supervisor communicate over newline-delimited JSON. Current messages use version `1` envelopes with `version`, `message_id`, `agent_id`, `turn_id`, `timestamp`, and `payload`. Unknown event names and payload fields are retained. Only versioned JSONL envelopes are accepted on the worker protocol.
9494
9595 A completed worker emits a `turn.result` event and writes `result.json`. The inline output is bounded; inspect the full session through the stable references:
9696
M packages/agent/args.go +16 -17
@@ -20,14 +20,13 @@ const (
2020 ModeStream Mode = "stream"
2121 ModeJSON Mode = "json"
2222 ModeRPC Mode = "rpc"
23- // ModeSwarmAgent is the long-lived, headless daemon mode used by
24- // swarm-spawned agents. The binary opens a unix-socket inbox at
25- // the path provided by --subagent-worker, reads supervisor messages
26- // off it (versioned JSONL, with a legacy text reader), runs each user
27- // turn against a persistent session, and streams JSONL events on
28- // stdout. --swarm-agent remains an input compatibility alias.
23+ // ModeSubagentWorker is the long-lived, headless daemon mode used by
24+ // subagent-spawned agents. The binary opens a unix-socket inbox at
25+ // the path provided by --subagent-worker, reads versioned JSONL
26+ // supervisor messages, runs each user turn against a persistent session,
27+ // and streams JSONL events on
28+ // stdout.
2929 ModeSubagentWorker Mode = "subagent-worker"
30- ModeSwarmAgent Mode = ModeSubagentWorker
3130 )
3231
3332 // Args holds parsed command-line options.
@@ -37,7 +36,7 @@ type Args struct {
3736 Model string
3837 APIKey string
3938
40- // inheritedCredential is populated only by swarm-agent mode from
39+ // inheritedCredential is populated only by subagent-worker mode from
4140 // the supervisor's stdin. It is never accepted as a CLI argument.
4241 inheritedCredential string
4342 inheritedAuthMethod string
@@ -49,11 +48,11 @@ type Args struct {
4948 Reasoning string
5049 Temperature *float32
5150
52- // FastMode is an internal swarm-child propagation flag. Normal users
51+ // FastMode is an internal subagent-child propagation flag. Normal users
5352 // enable fast mode through the persisted config /settings toggle.
5453 FastMode bool
5554 // FastModeSet distinguishes an explicit child override (including
56- // --no-fast-mode) from an omitted flag so persisted swarm settings
55+ // --no-fast-mode) from an omitted flag so persisted subagent settings
5756 // survive a parent config change.
5857 FastModeSet bool
5958
@@ -124,16 +123,16 @@ type Args struct {
124123 // mode auto-submits it once at startup before InitialInput handling.
125124 StartupPre string
126125
127- // SwarmAgent is the inbox-socket path when this process is a
126+ // SubagentWorker is the inbox-socket path when this process is a
128127 // subagent worker. Empty in every other mode. Set by either
129- // --subagent-worker or the --swarm-agent compatibility alias.
130- SwarmAgent string
128+ // --subagent-worker.
129+ SubagentWorker string
131130
132131 // SubagentMaxTurns limits prompt-level turns in worker mode.
133132 SubagentMaxTurns int
134133
135- // Subagent selects a named markdown profile for a swarm child.
136- // It is intentionally an internal child flag; the parent swarm tool
134+ // Subagent selects a named markdown profile for a subagent child.
135+ // It is intentionally an internal child flag; the parent subagent tool
137136 // passes only the profile name and the child discovers the definition
138137 // locally.
139138 Subagent string
@@ -299,12 +298,12 @@ func ParseArgs(in []string) (Args, error) {
299298 return a, err
300299 }
301300 a.CWD = v
302- case "--subagent-worker", "--swarm-agent":
301+ case "--subagent-worker":
303302 v, err := want(&i, arg)
304303 if err != nil {
305304 return a, err
306305 }
307- a.SwarmAgent = v
306+ a.SubagentWorker = v
308307 a.Mode = ModeSubagentWorker
309308 case "--subagent":
310309 v, err := want(&i, arg)
M packages/agent/args_test.go +2 -2
@@ -3,11 +3,11 @@ package agent
33 import "testing"
44
55 func TestParseArgsSubagentAndReasoning(t *testing.T) {
6- args, err := ParseArgs([]string{"--swarm-agent", "/tmp/in.sock", "--subagent", "reviewer", "--reasoning", "high", "task"})
6+ args, err := ParseArgs([]string{"--subagent-worker", "/tmp/in.sock", "--subagent", "reviewer", "--reasoning", "high", "task"})
77 if err != nil {
88 t.Fatal(err)
99 }
10- if args.Mode != ModeSwarmAgent || args.Subagent != "reviewer" || args.Reasoning != "high" || args.Prompt != "task" {
10+ if args.Mode != ModeSubagentWorker || args.Subagent != "reviewer" || args.Reasoning != "high" || args.Prompt != "task" {
1111 t.Fatalf("parsed args = mode=%q subagent=%q reasoning=%q prompt=%q", args.Mode, args.Subagent, args.Reasoning, args.Prompt)
1212 }
1313 }
M packages/agent/build.go +5 -5
@@ -691,7 +691,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
691691 if args.PermissionSet != nil {
692692 sandbox.SetPermissions(args.PermissionSet)
693693 }
694- subagentSession := args.Mode == ModeSwarmAgent || strings.TrimSpace(args.Subagent) != ""
694+ subagentSession := args.Mode == ModeSubagentWorker || strings.TrimSpace(args.Subagent) != ""
695695 lspEnabled := !args.NoLSP && cfg.LSPEnabledFor(subagentSession)
696696 reg := buildToolRegistry(args, args.CWD, sandbox, lspEnabled, cfg.LSPDiagnosticsOnWriteEnabled(subagentSession), cfg.LSPDiagnosticsOnEditEnabled(subagentSession))
697697
@@ -737,13 +737,13 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
737737 append_ = append(append_, skillAddendum)
738738 }
739739 interactiveMode := args.Mode == "" || args.Mode == ModeInteractive
740- if selectedProfile == nil && interactiveMode && autoSwarmToolAllowed(args) && cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled {
740+ if selectedProfile == nil && interactiveMode && autoSubagentsToolAllowed(args) && cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {
741741 homeDir, _ := os.UserHomeDir()
742742 profiles, _ := subagents.Discover(args.CWD, homeDir)
743743 if subagentsAddendum := subagents.SystemPromptAddendum(profiles); subagentsAddendum != "" {
744744 append_ = append(append_, subagentsAddendum)
745745 }
746- append_ = append(append_, AutoSwarmSystemAddendum)
746+ append_ = append(append_, AutoSubagentsSystemAddendum)
747747 }
748748 if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" {
749749 append_ = append(append_, selectedProfile.SystemPrompt)
@@ -1188,7 +1188,7 @@ func lspManagerNeeded(args Args, diagnosticsOnWrite, diagnosticsOnEdit bool) boo
11881188 return false
11891189 }
11901190
1191-func autoSwarmToolAllowed(args Args) bool {
1191+func autoSubagentsToolAllowed(args Args) bool {
11921192 if args.NoTools || args.PermissionSet != nil {
11931193 return false
11941194 }
@@ -1196,7 +1196,7 @@ func autoSwarmToolAllowed(args Args) bool {
11961196 return true
11971197 }
11981198 for _, name := range args.Tools {
1199- if name == "subagent_spawn" || name == "swarm_spawn" {
1199+ if name == "subagent_spawn" {
12001200 return true
12011201 }
12021202 }
M packages/agent/build_test.go +3 -3
@@ -156,7 +156,7 @@ func TestFindSubagentProfileReportsDiscoveryFailure(t *testing.T) {
156156 }
157157 }
158158
159-func TestResolveIncludesNamedSubagentsListWhenAutoSwarmIsEnabled(t *testing.T) {
159+func TestResolveIncludesNamedSubagentsListWhenAutoSubagentsIsEnabled(t *testing.T) {
160160 root := t.TempDir()
161161 home := filepath.Join(root, "home")
162162 zotHome := filepath.Join(root, "zot-home")
@@ -171,7 +171,7 @@ func TestResolveIncludesNamedSubagentsListWhenAutoSwarmIsEnabled(t *testing.T) {
171171 t.Setenv("ZOT_HOME", zotHome)
172172 t.Setenv("ZOT_AGENT_PROFILES", filepath.Join(home, ".agents", "agents"))
173173 enabled := true
174- if err := SaveConfig(Config{Provider: "openai", Model: "gpt-5", AutoSwarmEnabled: &enabled}); err != nil {
174+ if err := SaveConfig(Config{Provider: "openai", Model: "gpt-5", AutoSubagentsEnabled: &enabled}); err != nil {
175175 t.Fatal(err)
176176 }
177177 profile := `---
@@ -510,7 +510,7 @@ func TestResolveOllamaUsesModelBaseURLBeforeDefault(t *testing.T) {
510510 }
511511 }
512512
513-func TestResolveUsesInheritedSwarmCredential(t *testing.T) {
513+func TestResolveUsesInheritedSupervisorCredential(t *testing.T) {
514514 t.Setenv("ZOT_HOME", t.TempDir())
515515 t.Setenv("OPENAI_API_KEY", "")
516516
M packages/agent/cli.go +122 -127
@@ -21,7 +21,6 @@ import (
2121 "github.com/patriceckhart/zot/packages/agent/modes"
2222 "github.com/patriceckhart/zot/packages/agent/skills"
2323 "github.com/patriceckhart/zot/packages/agent/subagents"
24- "github.com/patriceckhart/zot/packages/agent/swarm"
2524 "github.com/patriceckhart/zot/packages/agent/tools"
2625 "github.com/patriceckhart/zot/packages/core"
2726 "github.com/patriceckhart/zot/packages/provider"
@@ -737,8 +736,8 @@ func runWithArgs(args Args, version string) error {
737736 return runJSONMode(ctx, args, version)
738737 case ModeRPC:
739738 return runRPCMode(ctx, args, version)
740- case ModeSwarmAgent:
741- return runSwarmAgentMode(ctx, args, version)
739+ case ModeSubagentWorker:
740+ return runSubagentWorkerMode(ctx, args, version)
742741 default:
743742 return runInteractive(ctx, args, version)
744743 }
@@ -1142,7 +1141,7 @@ func runZotfileStartupPre(ctx context.Context, pre, cwd string, sandbox *tools.S
11421141 // skills and currently loaded extension tools) and updates the live
11431142 // agent's registry and system prompt. Used after /reload-ext and after
11441143 // zotfile entry.pre installs new skills or extensions.
1145-// mutateRegistry, if non-nil, can inject session-specific tools (e.g. swarm_spawn).
1144+// mutateRegistry, if non-nil, can inject session-specific tools (e.g. subagent_spawn).
11461145 func refreshAgentToolsAndPrompt(args Args, sharedSandbox *tools.Sandbox, extToolAdapter ExtensionToolSource, ag *core.Agent, mutateRegistry func(core.Registry) core.Registry) {
11471146 if ag == nil {
11481147 return
@@ -1183,7 +1182,7 @@ func reloadResourcesAfterStartupPre(ctx context.Context, args Args, extMgr *exte
11831182
11841183 // ---- interactive mode: opens the TUI even without credentials ----
11851184
1186-func subagentPolicyFromConfig(cfg SubagentsConfig) swarm.SubagentPolicy {
1185+func subagentPolicyFromConfig(cfg SubagentsConfig) subagents.SubagentPolicy {
11871186 parseDuration := func(value string) time.Duration {
11881187 if strings.TrimSpace(value) == "" {
11891188 return 0
@@ -1194,7 +1193,7 @@ func subagentPolicyFromConfig(cfg SubagentsConfig) swarm.SubagentPolicy {
11941193 }
11951194 return parsed
11961195 }
1197- return swarm.SubagentPolicy{
1196+ return subagents.SubagentPolicy{
11981197 MaxConcurrent: cfg.MaxConcurrent,
11991198 MaxConcurrentPerParent: cfg.MaxConcurrentPerParent,
12001199 MaxTotalSpawned: cfg.MaxTotalSpawned,
@@ -1231,7 +1230,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
12311230
12321231 // persistMu guards the active session and selection across the agent
12331232 // loop, TUI callbacks, and session swaps. Declare it before tool
1234- // construction because swarm defaults must read the live selection too.
1233+ // construction because subagent defaults must read the live selection too.
12351234 var persistMu sync.Mutex
12361235 // Serialize session transitions with persistence callbacks so a candidate
12371236 // is read only after the old agent is flushed and cannot be overwritten by
@@ -1275,29 +1274,29 @@ func runInteractive(ctx context.Context, args Args, version string) error {
12751274 extToolAdapter := &extToolAdapter{mgr: extMgr}
12761275 r.MergeExtensionTools(extToolAdapter)
12771276
1278- // Build the swarm supervisor BEFORE the agent so the auto-swarm
1277+ // Build the subagent supervisor BEFORE the agent so the auto-subagents
12791278 // tool can reference it during tool-registry construction. State
1280- // lives under ZotHome/swarm so per-agent meta/events survive
1279+ // lives under ZotHome/subagents so per-agent meta/events survive
12811280 // restarts; the user can hunt orphaned agents down with
12821281 // `git worktree list` if anything misbehaves.
12831282 //
1284- // swarmMgr is also captured by loadSession / changeCWD closures
1283+ // subagentsMgr is also captured by loadSession / changeCWD closures
12851284 // further down the function, which is why we keep the variable
12861285 // in this outer scope rather than scoping it tighter.
1287- var swarmMgr *swarm.Swarm
1288- swarmMgr = swarm.New(swarm.Config{
1286+ var subagentsMgr *subagents.Supervisor
1287+ subagentsMgr = subagents.New(subagents.Config{
12891288 Context: ctx,
1290- Root: filepath.Join(ZotHome(), "swarm"),
1289+ Root: filepath.Join(ZotHome(), "subagents"),
12911290 RepoRoot: r.CWD,
12921291 Provider: r.Provider,
12931292 FastMode: r.FastMode,
12941293 BaseURL: r.BaseURL,
12951294 InsecureTLS: r.InsecureTLS,
12961295 Policy: subagentPolicyFromConfig(initialCfg.Subagents),
1297- // Keep an explicit launch key in this callback only. Swarm persists
1296+ // Keep an explicit launch key in this callback only. Supervisor persists
12981297 // connection settings, but never stores credentials in Agent metadata
12991298 // or puts them in the worker's argv/environment.
1300- ResolveCredential: func(ctx context.Context, providerID string) (swarm.Credential, error) {
1299+ ResolveCredential: func(ctx context.Context, providerID string) (subagents.Credential, error) {
13011300 childProvider := canonicalProvider(providerID)
13021301 persistMu.Lock()
13031302 hostProvider := canonicalProvider(activeProvider)
@@ -1313,62 +1312,61 @@ func runInteractive(ctx context.Context, args Args, version string) error {
13131312 explicit = args.APIKey
13141313 }
13151314 if childProvider == "ollama" && explicit == "" {
1316- return swarm.Credential{Value: "ollama", Method: "apikey"}, nil
1315+ return subagents.Credential{Value: "ollama", Method: "apikey"}, nil
13171316 }
13181317 credential, method, accountID, err := ResolveCredentialFullContext(ctx, childProvider, explicit)
13191318 if err != nil {
13201319 // Providers backed only by a local endpoint do not need a key.
13211320 // Leave stdin untouched and let the child resolve that endpoint.
13221321 if !CredentialAvailable(childProvider) {
1323- return swarm.Credential{}, nil
1322+ return subagents.Credential{}, nil
13241323 }
1325- return swarm.Credential{}, err
1324+ return subagents.Credential{}, err
13261325 }
1327- return swarm.Credential{Value: credential, Method: method, AccountID: accountID}, nil
1326+ return subagents.Credential{Value: credential, Method: method, AccountID: accountID}, nil
13281327 },
13291328 })
13301329 // Pull any previously-spawned agents off disk so the dashboard
13311330 // shows them as detached and the user can resume / remove them.
1332- _, _ = swarmMgr.Reload()
1331+ _, _ = subagentsMgr.Reload()
13331332 if args.PermissionSet != nil {
13341333 // Packaged zotfile agents have an explicit capability ceiling. A
13351334 // child process would otherwise rebuild a fresh unrestricted sandbox,
13361335 // so do not expose either slash or tool-based delegation from this
13371336 // restricted host until worker capability propagation exists.
1338- swarmMgr = nil
1337+ subagentsMgr = nil
13391338 }
13401339
1341- // onSpawnedSwarm is the OnSpawned callback the swarm_spawn tool
1340+ // onSpawnedSupervisor is the OnSpawned callback the subagent_spawn tool
13421341 // fires after every successful spawn. It hands the agent off to
13431342 // the running Interactive so the watcher can flush a summary back
13441343 // into chat when all sub-agents finish. Reads `iv` lazily because
13451344 // the Interactive is constructed after the agent.
1346- onSpawnedSwarm := func(a *swarm.Agent, task string) {
1345+ onSpawnedSupervisor := func(a *subagents.Agent, task string) {
13471346 if iv != nil {
1348- iv.TrackSwarmAgent(a, task)
1347+ iv.TrackSubagentWorker(a, task)
13491348 }
13501349 }
13511350 resolveSubagent := func(name string) (*subagents.Profile, error) {
13521351 return findSubagentProfile(r.CWD, name)
13531352 }
13541353
1355- // Inject the swarm_spawn auto-swarm tool only when /settings ->
1356- // auto-swarm is currently enabled. Registering it unconditionally
1354+ // Inject the subagent_spawn auto-subagents tool only when /settings ->
1355+ // auto-subagents is currently enabled. Registering it unconditionally
13571356 // leaves the model trying to call it (and getting a polite error)
13581357 // even when the user has switched the feature off. The /settings
13591358 // toggle live-mutates the running agent's registry separately so
13601359 // flipping the flag mid-session takes effect on the next turn.
1361- injectSwarmSpawn := func(reg core.Registry) core.Registry {
1362- if reg == nil || swarmMgr == nil || !autoSwarmToolAllowed(args) {
1360+ injectSubagentSpawn := func(reg core.Registry) core.Registry {
1361+ if reg == nil || subagentsMgr == nil || !autoSubagentsToolAllowed(args) {
13631362 return reg
13641363 }
1365- if !AutoSwarmEnabled() {
1364+ if !AutoSubagentsEnabled() {
13661365 return reg
13671366 }
1368- canonical := &tools.SwarmSpawnTool{
1369- ToolName: "subagent_spawn",
1370- Swarm: swarmMgr,
1371- Enabled: AutoSwarmEnabled,
1367+ canonical := &tools.SubagentSpawnTool{
1368+ Supervisor: subagentsMgr,
1369+ Enabled: AutoSubagentsEnabled,
13721370 DefaultModel: func() string {
13731371 persistMu.Lock()
13741372 defer persistMu.Unlock()
@@ -1381,15 +1379,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
13811379 },
13821380 DefaultReasoning: func() string { return r.Reasoning },
13831381 ResolveSubagent: resolveSubagent,
1384- OnSpawned: onSpawnedSwarm,
1382+ OnSpawned: onSpawnedSupervisor,
13851383 }
13861384 reg[canonical.Name()] = canonical
1387- legacy := *canonical
1388- legacy.ToolName = "swarm_spawn"
1389- reg[legacy.Name()] = &legacy
13901385 return reg
13911386 }
1392- injectSwarmSpawn(r.ToolRegistry)
1387+ injectSubagentSpawn(r.ToolRegistry)
13931388
13941389 // Confirmation gate: when --no-yolo is on, the agent must ask
13951390 // the user before every tool call. In interactive mode the TUI
@@ -1471,12 +1466,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
14711466 return nil, "", "", err
14721467 }
14731468 resolved.UseSandbox(sharedSandbox)
1474- if swarmMgr != nil {
1475- swarmMgr.SetProvider(resolved.Provider)
1476- swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
1469+ if subagentsMgr != nil {
1470+ subagentsMgr.SetProvider(resolved.Provider)
1471+ subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
14771472 }
14781473 resolved.MergeExtensionTools(extToolAdapter)
1479- injectSwarmSpawn(resolved.ToolRegistry)
1474+ injectSubagentSpawn(resolved.ToolRegistry)
14801475 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil
14811476 }
14821477
@@ -1494,12 +1489,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
14941489 return nil, "", "", err
14951490 }
14961491 resolved.UseSandbox(sharedSandbox)
1497- if swarmMgr != nil {
1498- swarmMgr.SetProvider(resolved.Provider)
1499- swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
1492+ if subagentsMgr != nil {
1493+ subagentsMgr.SetProvider(resolved.Provider)
1494+ subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
15001495 }
15011496 resolved.MergeExtensionTools(extToolAdapter)
1502- injectSwarmSpawn(resolved.ToolRegistry)
1497+ injectSubagentSpawn(resolved.ToolRegistry)
15031498 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil
15041499 }
15051500
@@ -1525,12 +1520,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
15251520 return nil, "", "", err
15261521 }
15271522 resolved.UseSandbox(sharedSandbox)
1528- if swarmMgr != nil {
1529- swarmMgr.SetProvider(resolved.Provider)
1530- swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
1523+ if subagentsMgr != nil {
1524+ subagentsMgr.SetProvider(resolved.Provider)
1525+ subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
15311526 }
15321527 resolved.MergeExtensionTools(extToolAdapter)
1533- injectSwarmSpawn(resolved.ToolRegistry)
1528+ injectSubagentSpawn(resolved.ToolRegistry)
15341529 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil
15351530 }
15361531
@@ -1549,7 +1544,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
15491544 if current == nil {
15501545 return
15511546 }
1552- refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn)
1547+ refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn)
15531548 })
15541549
15551550 var sess *core.Session
@@ -1772,10 +1767,10 @@ func runInteractive(ctx context.Context, args Args, version string) error {
17721767 }
17731768 activeProvider = candidate.provider
17741769 activeModel = candidate.model
1775- if swarmMgr != nil && candidate.session != nil {
1770+ if subagentsMgr != nil && candidate.session != nil {
17761771 // Keep the dashboard scope in the same commit as the session,
17771772 // agent, usage, and persistence baseline.
1778- swarmMgr.SetActiveSession(candidate.session.ID)
1773+ subagentsMgr.SetActiveSession(candidate.session.ID)
17791774 }
17801775 committed = true
17811776 persistMu.Unlock()
@@ -1794,7 +1789,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
17941789 //
17951790 // Steps, in order:
17961791 // 1. resolve + validate the new path (~ expansion, abs/rel)
1797- // 2. stage the candidate cwd, sandbox, and swarm state
1792+ // 2. stage the candidate cwd, sandbox, and subagent state
17981793 // 3. rebuild the agent and allocate its new session
17991794 // 4. commit all new state together, closing the old session last
18001795 // 5. push the committed state into the running Interactive
@@ -1841,7 +1836,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
18411836
18421837 // Stage the candidate state while retaining a complete rollback
18431838 // snapshot. A failed rebuild or session allocation must leave the
1844- // current agent, session, sandbox, and swarm root usable.
1839+ // current agent, session, sandbox, and subagent root usable.
18451840 oldCWD := args.CWD
18461841 oldResolvedCWD := r.CWD
18471842 oldPermissionSet := args.PermissionSet
@@ -1849,9 +1844,9 @@ func runInteractive(ctx context.Context, args Args, version string) error {
18491844 oldActiveProvider := activeProvider
18501845 oldActiveModel := activeModel
18511846 persistMu.Unlock()
1852- oldSwarmRoot := ""
1853- if swarmMgr != nil {
1854- oldSwarmRoot = swarmMgr.RepoRoot()
1847+ oldSupervisorRoot := ""
1848+ if subagentsMgr != nil {
1849+ oldSupervisorRoot = subagentsMgr.RepoRoot()
18551850 }
18561851 oldSandboxRoot := ""
18571852 oldSandboxLocked := false
@@ -1867,8 +1862,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
18671862 activeProvider = oldActiveProvider
18681863 activeModel = oldActiveModel
18691864 persistMu.Unlock()
1870- if swarmMgr != nil {
1871- swarmMgr.SetRepoRoot(oldSwarmRoot)
1865+ if subagentsMgr != nil {
1866+ subagentsMgr.SetRepoRoot(oldSupervisorRoot)
18721867 }
18731868 if sharedSandbox != nil {
18741869 sharedSandbox.Root = oldSandboxRoot
@@ -1882,8 +1877,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
18821877
18831878 args.CWD = absPath
18841879 r.CWD = absPath
1885- if swarmMgr != nil {
1886- swarmMgr.SetRepoRoot(absPath)
1880+ if subagentsMgr != nil {
1881+ subagentsMgr.SetRepoRoot(absPath)
18871882 }
18881883 if args.PermissionSet != nil {
18891884 expanded := args.PermissionSet.Expand(absPath, args.AgentDataDir)
@@ -1956,9 +1951,9 @@ func runInteractive(ctx context.Context, args Args, version string) error {
19561951 iv.ApplyChangedCWDWithStartupContext(newAg, newProvider, newModel, absPath, startupPaths)
19571952 }
19581953
1959- // Re-scope the swarm dashboard to the new session.
1960- if swarmMgr != nil && newSess != nil {
1961- swarmMgr.SetActiveSession(newSess.ID)
1954+ // Re-scope the subagent dashboard to the new session.
1955+ if subagentsMgr != nil && newSess != nil {
1956+ subagentsMgr.SetActiveSession(newSess.ID)
19621957 }
19631958 if newSess != nil {
19641959 extMgr.EmitSessionEvent("session_opened", sessionContext(newSess), newStates)
@@ -2033,19 +2028,19 @@ func runInteractive(ctx context.Context, args Args, version string) error {
20332028 }
20342029 }
20352030
2036- // swarmMgr was constructed and reloaded earlier (before the agent
2037- // build, so the auto-swarm tool could capture it). Here we just
2038- // scope the dashboard to the active host session so /swarm only
2031+ // subagentsMgr was constructed and reloaded earlier (before the agent
2032+ // build, so the auto-subagents tool could capture it). Here we just
2033+ // scope the dashboard to the active host session so /subagents only
20392034 // shows agents this session spawned (and any pre-upgrade unscoped
20402035 // agents — see SnapshotAll docs). Updated again whenever the
20412036 // user swaps sessions via loadSession below.
2042- if sess != nil && swarmMgr != nil {
2043- swarmMgr.SetActiveSession(sess.ID)
2037+ if sess != nil && subagentsMgr != nil {
2038+ subagentsMgr.SetActiveSession(sess.ID)
20442039 }
20452040 // Best-effort shutdown on interactive exit: stop all running
20462041 // agents so they don't outlive their parent zot.
2047- if swarmMgr != nil {
2048- defer swarmMgr.StopAll()
2042+ if subagentsMgr != nil {
2043+ defer subagentsMgr.StopAll()
20492044 }
20502045
20512046 var startupSkills []*skills.Skill
@@ -2055,56 +2050,56 @@ func runInteractive(ctx context.Context, args Args, version string) error {
20552050 homeDir, _ := os.UserHomeDir()
20562051 discoveredSubagents, _ := subagents.Discover(r.CWD, homeDir)
20572052 subagentsAddendum := subagents.SystemPromptAddendum(discoveredSubagents)
2058- autoSwarmToolAllowedForSession := autoSwarmToolAllowed(args)
2053+ autoSubagentsToolAllowedForSession := autoSubagentsToolAllowed(args)
20592054
20602055 iv = modes.NewInteractive(modes.InteractiveConfig{
2061- Terminal: term,
2062- Theme: theme,
2063- InlineImagesEnabled: initialCfg.InlineImagesEnabled,
2064- TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled,
2065- TerminalTitleEnabled: initialCfg.TerminalTitleEnabled,
2066- AutoSwarmEnabled: initialCfg.AutoSwarmEnabled,
2067- AutoSwarmToolAllowed: &autoSwarmToolAllowedForSession,
2068- FastMode: &fastMode,
2069- LSPEnabled: initialCfg.LSPEnabled,
2070- SubagentLSPEnabled: initialCfg.SubagentLSPEnabled,
2071- AutoCompactThreshold: initialCfg.AutoCompactThreshold,
2072- JailByDefault: initialCfg.JailByDefault,
2073- QuickModelShortcuts: quickModelShortcuts,
2074- RecursiveFileSuggest: initialCfg.RecursiveFileSuggest,
2075- RespectGitignore: initialCfg.RespectGitignore,
2076- CompactMode: initialCfg.CompactMode,
2077- TUIInputStyle: initialCfg.TUIInputStyle,
2078- TUIStatusPosition: initialCfg.TUIStatusPosition,
2079- TUIWorkingPosition: initialCfg.TUIWorkingPosition,
2080- ThemeName: initialCfg.Theme,
2081- FlatTools: initialCfg.FlatToolRender(),
2082- CompactUser: initialCfg.CompactUserInput(),
2083- ExtensionThemes: extMgr.ThemeOptions,
2084- AutoSwarmSystemAddendum: AutoSwarmSystemAddendum,
2085- SubagentsSystemAddendum: subagentsAddendum,
2086- SettingsStore: configSettingsStore{},
2087- Model: r.Model,
2088- Provider: r.Provider,
2089- AuthMethod: r.AuthMethod,
2090- BaseURL: r.BaseURL,
2091- Reasoning: r.Reasoning,
2092- SystemPrompt: r.SystemPrompt,
2093- Tools: r.ToolRegistry,
2094- MaxSteps: r.MaxSteps,
2095- CWD: r.CWD,
2096- StartupAgentName: args.AgentName,
2097- StartupContextPaths: instructionContextPaths(r.ContextFiles),
2098- StartupExtensionNames: startupExtensionNames(extMgr.All()),
2099- StartupExtensionErrors: startupExtensionErrors,
2100- StartupSkillNames: startupSkillNames(startupSkills),
2101- ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup,
2102- ZotHome: ZotHome(),
2103- SessionsRoot: agentSessionsRoot(ZotHome(), args),
2104- Version: version,
2105- UpdateInfoChan: updateCh,
2106- Sandbox: sharedSandbox,
2107- Agent: ag,
2056+ Terminal: term,
2057+ Theme: theme,
2058+ InlineImagesEnabled: initialCfg.InlineImagesEnabled,
2059+ TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled,
2060+ TerminalTitleEnabled: initialCfg.TerminalTitleEnabled,
2061+ AutoSubagentsEnabled: initialCfg.AutoSubagentsEnabled,
2062+ AutoSubagentsToolAllowed: &autoSubagentsToolAllowedForSession,
2063+ FastMode: &fastMode,
2064+ LSPEnabled: initialCfg.LSPEnabled,
2065+ SubagentLSPEnabled: initialCfg.SubagentLSPEnabled,
2066+ AutoCompactThreshold: initialCfg.AutoCompactThreshold,
2067+ JailByDefault: initialCfg.JailByDefault,
2068+ QuickModelShortcuts: quickModelShortcuts,
2069+ RecursiveFileSuggest: initialCfg.RecursiveFileSuggest,
2070+ RespectGitignore: initialCfg.RespectGitignore,
2071+ CompactMode: initialCfg.CompactMode,
2072+ TUIInputStyle: initialCfg.TUIInputStyle,
2073+ TUIStatusPosition: initialCfg.TUIStatusPosition,
2074+ TUIWorkingPosition: initialCfg.TUIWorkingPosition,
2075+ ThemeName: initialCfg.Theme,
2076+ FlatTools: initialCfg.FlatToolRender(),
2077+ CompactUser: initialCfg.CompactUserInput(),
2078+ ExtensionThemes: extMgr.ThemeOptions,
2079+ AutoSubagentsSystemAddendum: AutoSubagentsSystemAddendum,
2080+ SubagentsSystemAddendum: subagentsAddendum,
2081+ SettingsStore: configSettingsStore{},
2082+ Model: r.Model,
2083+ Provider: r.Provider,
2084+ AuthMethod: r.AuthMethod,
2085+ BaseURL: r.BaseURL,
2086+ Reasoning: r.Reasoning,
2087+ SystemPrompt: r.SystemPrompt,
2088+ Tools: r.ToolRegistry,
2089+ MaxSteps: r.MaxSteps,
2090+ CWD: r.CWD,
2091+ StartupAgentName: args.AgentName,
2092+ StartupContextPaths: instructionContextPaths(r.ContextFiles),
2093+ StartupExtensionNames: startupExtensionNames(extMgr.All()),
2094+ StartupExtensionErrors: startupExtensionErrors,
2095+ StartupSkillNames: startupSkillNames(startupSkills),
2096+ ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup,
2097+ ZotHome: ZotHome(),
2098+ SessionsRoot: agentSessionsRoot(ZotHome(), args),
2099+ Version: version,
2100+ UpdateInfoChan: updateCh,
2101+ Sandbox: sharedSandbox,
2102+ Agent: ag,
21082103 InitialSessionTitle: func() string {
21092104 if sess == nil || sessionTitlePending {
21102105 return ""
@@ -2122,12 +2117,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
21222117 _ = extMgr.Reload(context.Background(), 2*time.Second)
21232118 }
21242119 if current != nil {
2125- refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn)
2120+ refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn)
21262121 }
21272122 },
21282123 RefreshTools: func() {
21292124 current := liveInteractiveAgent(iv, ag)
2130- refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn)
2125+ refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn)
21312126 },
21322127 AuthManager: mgr,
21332128 LlamaCPPConfig: ResolveLlamaCPPConfig,
@@ -2193,7 +2188,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
21932188 },
21942189 SessionTransition: newSessionTransition(&sessionTransitionMu),
21952190 Extensions: extMgr,
2196- Swarm: swarmMgr,
2191+ Supervisor: subagentsMgr,
21972192 ResolveSubagent: resolveSubagent,
21982193 ChangelogChan: changelogCh,
21992194 OnChangelogDismiss: func() {
@@ -2321,8 +2316,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
23212316 // cleanup, so relying only on the normal runInteractive defer would
23222317 // leave child daemons (and their descendants) alive after a TERM or
23232318 // HUP delivered to the host.
2324- if swarmMgr != nil {
2325- swarmMgr.StopAll()
2319+ if subagentsMgr != nil {
2320+ subagentsMgr.StopAll()
23262321 }
23272322 // Exit cleanly. Re-raising the signal would skip os.Exit's
23282323 // at-exit hooks; explicit exit is fine because we've already
@@ -2371,10 +2366,10 @@ func openOrCreateSession(args Args, r Resolved, ag *core.Agent, version string)
23712366 switch {
23722367 case args.Session != "":
23732368 s, msgs, err = core.OpenSession(args.Session)
2374- // The swarm-agent child passes a fixed --session path that
2369+ // The subagent-worker child passes a fixed --session path that
23752370 // may not exist yet on first Spawn. Treat ENOENT as "create
23762371 // a fresh session AT THIS PATH" so the conversation actually
2377- // gets persisted; without this fallback the swarm child runs
2372+ // gets persisted; without this fallback the subagent child runs
23782373 // with sess==nil and every Resume re-starts with no memory
23792374 // of the prior turns. Other openers (--continue / --resume /
23802375 // the picker) never see ENOENT here because they only choose
M packages/agent/config.go +5 -5
@@ -90,17 +90,17 @@ type Config struct {
9090 // Subagents controls manager-owned child lifecycle and resource policy.
9191 Subagents SubagentsConfig `json:"subagents,omitempty"`
9292
93- // AutoSwarmEnabled lets the main agent spawn background sub-agents
94- // for parallel sub-tasks via a built-in swarm_spawn tool. Off by
93+ // AutoSubagentsEnabled lets the main agent spawn background sub-agents
94+ // for parallel sub-tasks via a built-in subagent_spawn tool. Off by
9595 // default; nil/missing means disabled. Toggle from /settings.
96- AutoSwarmEnabled *bool `json:"auto_swarm_enabled,omitempty"`
96+ AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"`
9797
9898 // LSPEnabled controls the built-in lsp tool and write-time diagnostics
9999 // for the main session. nil/missing means enabled. Toggle from
100100 // /settings.
101101 LSPEnabled *bool `json:"lsp_enabled,omitempty"`
102102
103- // SubagentLSPEnabled controls LSP availability in swarm sub-agent
103+ // SubagentLSPEnabled controls LSP availability in subagent worker
104104 // processes. nil/missing means enabled. Toggle from /settings.
105105 SubagentLSPEnabled *bool `json:"subagent_lsp_enabled,omitempty"`
106106
@@ -235,7 +235,7 @@ func (c Config) CompactUserInput() bool {
235235 return c.CompactInput != nil && *c.CompactInput
236236 }
237237
238-// LSPEnabledFor reports whether LSP is enabled for a main or swarm
238+// LSPEnabledFor reports whether LSP is enabled for a main or subagent
239239 // sub-agent session. Both settings default to true so new installations
240240 // get code intelligence without a migration or generated config file.
241241 func (c Config) LSPEnabledFor(subagent bool) bool {
R packages/agent/modes/auto_swarm_test.go → packages/agent/modes/auto_subagents_test.go +40 -40 renamed, 58% similar
@@ -7,64 +7,64 @@ import (
77 "testing"
88 "time"
99
10- "github.com/patriceckhart/zot/packages/agent/swarm"
10+ "github.com/patriceckhart/zot/packages/agent/subagents"
1111 "github.com/patriceckhart/zot/packages/core"
1212 )
1313
14-func TestAutoSwarmSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) {
14+func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) {
1515 iv := &Interactive{
1616 agent: &core.Agent{System: "base system"},
1717 cfg: InteractiveConfig{
18- AutoSwarmSystemAddendum: "auto-swarm guidance",
19- SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]",
18+ AutoSubagentsSystemAddendum: "auto-subagents guidance",
19+ SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]",
2020 },
2121 }
2222
23- iv.applyAutoSwarmSystemPrompt(true)
24- if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-swarm guidance") {
25- t.Fatalf("enabled system prompt missing swarm blocks: %q", iv.agent.System)
23+ iv.applyAutoSubagentsSystemPrompt(true)
24+ if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-subagents guidance") {
25+ t.Fatalf("enabled system prompt missing subagent blocks: %q", iv.agent.System)
2626 }
2727 before := iv.agent.System
28- iv.applyAutoSwarmSystemPrompt(true)
28+ iv.applyAutoSubagentsSystemPrompt(true)
2929 if iv.agent.System != before {
3030 t.Fatalf("enabling twice changed system prompt: %q -> %q", before, iv.agent.System)
3131 }
3232
33- iv.applyAutoSwarmSystemPrompt(false)
34- if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-swarm guidance") {
35- t.Fatalf("disabled system prompt retained swarm blocks: %q", iv.agent.System)
33+ iv.applyAutoSubagentsSystemPrompt(false)
34+ if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") {
35+ t.Fatalf("disabled system prompt retained subagent blocks: %q", iv.agent.System)
3636 }
3737 }
3838
39-func TestAutoSwarmTogglePreservesMatchingBasePromptBlocks(t *testing.T) {
39+func TestAutoSubagentsTogglePreservesMatchingBasePromptBlocks(t *testing.T) {
4040 manifest := "[subagents_list]\n- reviewer\n[/subagents_list]"
41- guidance := "auto-swarm guidance"
41+ guidance := "auto-subagents guidance"
4242 base := "base system\n\n" + manifest + "\n\n" + guidance
4343 iv := &Interactive{
4444 agent: &core.Agent{System: base},
4545 cfg: InteractiveConfig{
46- AutoSwarmSystemAddendum: guidance,
47- SubagentsSystemAddendum: manifest,
46+ AutoSubagentsSystemAddendum: guidance,
47+ SubagentsSystemAddendum: manifest,
4848 },
4949 }
5050
51- iv.applyAutoSwarmSystemPrompt(true)
51+ iv.applyAutoSubagentsSystemPrompt(true)
5252 if strings.Count(iv.agent.System, manifest) != 2 || strings.Count(iv.agent.System, guidance) != 2 {
5353 t.Fatalf("enabling should append owned duplicate blocks: %q", iv.agent.System)
5454 }
55- iv.applyAutoSwarmSystemPrompt(false)
55+ iv.applyAutoSubagentsSystemPrompt(false)
5656 if got := strings.TrimSpace(iv.agent.System); got != base {
5757 t.Fatalf("disabling changed matching base prompt: %q; want %q", got, base)
5858 }
5959 }
6060
61-func TestAutoSwarmSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T) {
61+func TestAutoSubagentsSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T) {
6262 response := "first response line\n" + strings.Repeat("result ", 150) + "final marker"
63- mgr := swarm.New(swarm.Config{
63+ mgr := subagents.New(subagents.Config{
6464 Root: t.TempDir(),
6565 RepoRoot: t.TempDir(),
66- NewRunner: func(*swarm.Agent) swarm.Runner {
67- return swarm.RunnerFunc(func(_ context.Context, sink swarm.Sink) error {
66+ NewRunner: func(*subagents.Agent) subagents.Runner {
67+ return subagents.RunnerFunc(func(_ context.Context, sink subagents.Sink) error {
6868 sink.Transcript(response)
6969 return nil
7070 })
@@ -75,20 +75,20 @@ func TestAutoSwarmSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T
7575 t.Fatal(err)
7676 }
7777
78- iv := newQueuedAutoSwarmInteractive()
79- iv.TrackSwarmAgent(a, a.Task)
78+ iv := newQueuedAutoSubagentsInteractive()
79+ iv.TrackSubagentWorker(a, a.Task)
8080 update := waitForQueuedPrompt(t, iv)
8181 if !strings.Contains(update, "final response:\n"+response) {
8282 t.Fatalf("update missing complete final response: %q", update)
8383 }
8484 }
8585
86-func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) {
87- mgr := swarm.New(swarm.Config{
86+func TestTrackSubagentWorkerReportsStartupFailure(t *testing.T) {
87+ mgr := subagents.New(subagents.Config{
8888 Root: t.TempDir(),
8989 RepoRoot: t.TempDir(),
90- NewRunner: func(*swarm.Agent) swarm.Runner {
91- return swarm.RunnerFunc(func(context.Context, swarm.Sink) error {
90+ NewRunner: func(*subagents.Agent) subagents.Runner {
91+ return subagents.RunnerFunc(func(context.Context, subagents.Sink) error {
9292 return errors.New("listener startup failed")
9393 })
9494 },
@@ -98,8 +98,8 @@ func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) {
9898 t.Fatal(err)
9999 }
100100
101- iv := newQueuedAutoSwarmInteractive()
102- iv.TrackSwarmAgent(a, a.Task)
101+ iv := newQueuedAutoSubagentsInteractive()
102+ iv.TrackSubagentWorker(a, a.Task)
103103
104104 update := waitForQueuedPrompt(t, iv)
105105 if !strings.Contains(update, "status: failed") {
@@ -110,13 +110,13 @@ func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) {
110110 }
111111 }
112112
113-func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) {
113+func TestCompleteSupervisorWatchReportsTurnOutcomeOnce(t *testing.T) {
114114 started := make(chan struct{})
115- mgr := swarm.New(swarm.Config{
115+ mgr := subagents.New(subagents.Config{
116116 Root: t.TempDir(),
117117 RepoRoot: t.TempDir(),
118- NewRunner: func(*swarm.Agent) swarm.Runner {
119- return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error {
118+ NewRunner: func(*subagents.Agent) subagents.Runner {
119+ return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error {
120120 close(started)
121121 <-ctx.Done()
122122 return ctx.Err()
@@ -133,11 +133,11 @@ func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) {
133133 a.Wait()
134134 })
135135
136- iv := newQueuedAutoSwarmInteractive()
137- entry := &swarmWatchEntry{agent: a, task: a.Task}
138- iv.swarmWatch = []*swarmWatchEntry{entry}
139- iv.completeSwarmWatchEntry(entry, "completed", "")
140- iv.completeSwarmWatchEntry(entry, "failed", "late error")
136+ iv := newQueuedAutoSubagentsInteractive()
137+ entry := &subagentWatchEntry{agent: a, task: a.Task}
138+ iv.subagentWatch = []*subagentWatchEntry{entry}
139+ iv.completeSupervisorWatchEntry(entry, "completed", "")
140+ iv.completeSupervisorWatchEntry(entry, "failed", "late error")
141141
142142 update := waitForQueuedPrompt(t, iv)
143143 if !strings.Contains(update, "status: completed") {
@@ -151,7 +151,7 @@ func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) {
151151 }
152152 }
153153
154-func newQueuedAutoSwarmInteractive() *Interactive {
154+func newQueuedAutoSubagentsInteractive() *Interactive {
155155 return &Interactive{
156156 agent: &core.Agent{},
157157 busy: true,
@@ -176,7 +176,7 @@ func waitForQueuedPrompt(t *testing.T, iv *Interactive) string {
176176 iv.mu.Unlock()
177177 select {
178178 case <-deadline.C:
179- t.Fatal("timed out waiting for auto-swarm update")
179+ t.Fatal("timed out waiting for auto-subagents update")
180180 case <-ticker.C:
181181 }
182182 }
M packages/agent/modes/dialog_text_test.go +7 -7
@@ -6,7 +6,7 @@ import (
66
77 "github.com/mattn/go-runewidth"
88
9- "github.com/patriceckhart/zot/packages/agent/swarm"
9+ "github.com/patriceckhart/zot/packages/agent/subagents"
1010 "github.com/patriceckhart/zot/packages/tui"
1111 )
1212
@@ -47,10 +47,10 @@ func TestBtwDialogWrapsAssistantAndKeepsCursorAligned(t *testing.T) {
4747 }
4848 }
4949
50-func TestSwarmTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) {
50+func TestSupervisorTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) {
5151 const width = 80
5252 long := strings.Repeat("long response text ", 8)
53- rows := renderSwarmTranscriptBlocks([]string{
53+ rows := renderSupervisorTranscriptBlocks([]string{
5454 long,
5555 "stderr: " + long,
5656 "error: " + long,
@@ -65,15 +65,15 @@ func TestSwarmTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) {
6565 }
6666 }
6767
68-func TestSwarmDialogViewingWrapsSpawnedAgentResponse(t *testing.T) {
68+func TestSubagentsDialogViewingWrapsSpawnedAgentResponse(t *testing.T) {
6969 const width = 80
7070 response := strings.Repeat("spawned agent response ", 6)
71- rows := []swarm.AgentSnapshot{{
71+ rows := []subagents.AgentSnapshot{{
7272 ID: "agent-1",
73- Status: swarm.StatusDone,
73+ Status: subagents.StatusDone,
7474 Lines: []string{response},
7575 }}
76- d := newSwarmDialog()
76+ d := newSubagentsDialog()
7777 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
7878 _ = d.Render(tui.Theme{}, width)
7979 d.HandleKey(tui.Key{Kind: tui.KeyEnter})
M packages/agent/modes/double_escape.go +1 -1
@@ -79,7 +79,7 @@ func (i *Interactive) sessionTreeEscapeBlocked() bool {
7979 i.llamaDialog != nil && i.llamaDialog.Active() ||
8080 i.rescueDialog != nil && i.rescueDialog.Active() ||
8181 i.sessionDialog != nil && i.sessionDialog.Active() ||
82- i.swarmDialog != nil && i.swarmDialog.Active() ||
82+ i.subagentsDialog != nil && i.subagentsDialog.Active() ||
8383 i.jumpDialog != nil && i.jumpDialog.Active() ||
8484 i.btwDialog != nil && i.btwDialog.Active() ||
8585 i.skillsDialog != nil && i.skillsDialog.Active() ||
M packages/agent/modes/interactive.go +133 -138
@@ -18,7 +18,6 @@ import (
1818 "github.com/patriceckhart/zot/packages/agent/modes/telegram"
1919 "github.com/patriceckhart/zot/packages/agent/skills"
2020 "github.com/patriceckhart/zot/packages/agent/subagents"
21- "github.com/patriceckhart/zot/packages/agent/swarm"
2221 "github.com/patriceckhart/zot/packages/agent/tools"
2322 "github.com/patriceckhart/zot/packages/core"
2423 "github.com/patriceckhart/zot/packages/provider"
@@ -68,14 +67,14 @@ type InteractiveConfig struct {
6867 // title updates. nil means enabled; false disables both.
6968 TerminalTitleEnabled *bool
7069
71- // AutoSwarmEnabled mirrors the persisted config flag at startup so
70+ // AutoSubagentsEnabled mirrors the persisted config flag at startup so
7271 // the /settings dialog can render the current state without
7372 // re-reading config.json on every open.
74- AutoSwarmEnabled *bool
73+ AutoSubagentsEnabled *bool
7574
76- // AutoSwarmToolAllowed is nil for normal sessions and false when the
75+ // AutoSubagentsToolAllowed is nil for normal sessions and false when the
7776 // launch-time --no-tools/--tools policy excludes delegation.
78- AutoSwarmToolAllowed *bool
77+ AutoSubagentsToolAllowed *bool
7978
8079 // FastMode mirrors the persisted OpenAI fast-mode flag at startup.
8180 // nil/missing means disabled. Unsupported providers reject attempts
@@ -86,7 +85,7 @@ type InteractiveConfig struct {
8685 // enabled by default.
8786 LSPEnabled *bool
8887
89- // SubagentLSPEnabled controls whether newly spawned swarm children
88+ // SubagentLSPEnabled controls whether newly spawned subagent children
9089 // receive the lsp tool. nil means enabled by default.
9190 SubagentLSPEnabled *bool
9291
@@ -145,14 +144,14 @@ type InteractiveConfig struct {
145144 // ExtensionThemes returns themes bundled with loaded extensions.
146145 ExtensionThemes func() []tui.ThemeOption
147146
148- // AutoSwarmSystemAddendum is the system-prompt block that gets
149- // appended/stripped when the user toggles auto-swarm at runtime.
147+ // AutoSubagentsSystemAddendum is the system-prompt block that gets
148+ // appended/stripped when the user toggles auto-subagents at runtime.
150149 // Plumbed in from the cli so this package doesn't have to import
151150 // agent (cycle).
152- AutoSwarmSystemAddendum string
151+ AutoSubagentsSystemAddendum string
153152
154153 // SubagentsSystemAddendum is the metadata-only [subagents_list]
155- // block that is added or removed together with auto-swarm.
154+ // block that is added or removed together with auto-subagents.
156155 SubagentsSystemAddendum string
157156 SettingsStore SettingsStore
158157
@@ -319,15 +318,15 @@ type InteractiveConfig struct {
319318 // AFTER the built-in catalog so a built-in name always wins.
320319 Extensions *extensions.Manager
321320
322- // Swarm, if non-nil, enables the /swarm slash command and the
323- // dashboard dialog. The cli constructs the Swarm once per
321+ // Supervisor, if non-nil, enables the /subagents slash command and the
322+ // dashboard dialog. The cli constructs the Supervisor once per
324323 // interactive run and tears it down on exit. nil disables the
325324 // feature entirely (used by embedders / tests that don't want
326325 // subprocesses).
327- Swarm *swarm.Swarm
326+ Supervisor *subagents.Supervisor
328327
329328 // ResolveSubagent validates a named markdown profile for direct
330- // /swarm commands. Auto-swarm uses the equivalent callback on its
329+ // /subagents commands. Auto-subagents uses the equivalent callback on its
331330 // tool; keeping this optional preserves lightweight embedders.
332331 ResolveSubagent func(name string) (*subagents.Profile, error)
333332
@@ -414,7 +413,7 @@ type extensionWidget struct {
414413 type SettingsStore interface {
415414 SetQuickModelShortcut(slot int, providerName, model string) error
416415 SetInlineImages(enabled bool) error
417- SetAutoSwarm(enabled bool) error
416+ SetAutoSubagents(enabled bool) error
418417 SetJailByDefault(enabled bool) error
419418 SetRecursiveFileSuggest(enabled bool) error
420419 SetRespectGitignore(enabled bool) error
@@ -459,13 +458,13 @@ type Interactive struct {
459458
460459 mu sync.Mutex
461460 agent *core.Agent
462- // managedAutoSwarmAddenda records exact prompt blocks appended by
463- // auto-swarm. Disable only removes these owned occurrences, leaving
461+ // managedAutoSubagentsAddenda records exact prompt blocks appended by
462+ // auto-subagents. Disable only removes these owned occurrences, leaving
464463 // identical text that came from the user's base prompt untouched.
465- managedAutoSwarmAddenda []string
466- streaming strings.Builder // what's currently painted on screen
467- streamOn bool
468- pendingAlert *extproto.AlertRequest
464+ managedAutoSubagentsAddenda []string
465+ streaming strings.Builder // what's currently painted on screen
466+ streamOn bool
467+ pendingAlert *extproto.AlertRequest
469468
470469 // streamPending is the runes buffered after each EvTextDelta that
471470 // haven't yet been promoted into `streaming` for rendering. It
@@ -576,7 +575,7 @@ type Interactive struct {
576575 llamaDialog *llamaDialog
577576 rescueDialog *rescueDialog
578577 sessionDialog *sessionDialog
579- swarmDialog *swarmDialog
578+ subagentsDialog *subagentsDialog
580579 jumpDialog *jumpDialog
581580 btwDialog *btwDialog
582581 skillsDialog *skillsDialog
@@ -592,14 +591,14 @@ type Interactive struct {
592591 extPanel *extPanelDialog
593592 llamaConfigured bool
594593
595- // swarmWatch tracks auto-swarm sub-agents the main agent spawned
596- // via swarm_spawn. Each entry holds the agent + the task text;
594+ // subagentWatch tracks auto-subagents sub-agents the main agent spawned
595+ // via subagent_spawn. Each entry holds the agent + the task text;
597596 // a per-entry goroutine waits on the agent's terminal state. When
598597 // every tracked entry has finished, the watcher flushes a single
599598 // summary turn into the main chat (queued if the main agent is
600599 // busy, run immediately if idle).
601- swarmWatchMu sync.Mutex
602- swarmWatch []*swarmWatchEntry
600+ subagentWatchMu sync.Mutex
601+ subagentWatch []*subagentWatchEntry
603602
604603 // pendingFork is true when the user ran /session fork: the next
605604 // jump-picker selection should branch off that message instead
@@ -756,7 +755,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
756755 llamaDialog: newLlamaDialog(),
757756 rescueDialog: newRescueDialog(),
758757 sessionDialog: newSessionDialog(),
759- swarmDialog: newSwarmDialog(),
758+ subagentsDialog: newSubagentsDialog(),
760759 jumpDialog: newJumpDialog(),
761760 btwDialog: newBtwDialog(),
762761 skillsDialog: newSkillsDialog(),
@@ -783,8 +782,8 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
783782 baseURL, _, err := cfg.LlamaCPPConfig()
784783 i.llamaConfigured = err == nil && baseURL != ""
785784 }
786- if cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled {
787- i.managedAutoSwarmAddenda = autoSwarmAddenda(cfg)
785+ if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {
786+ i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg)
788787 }
789788 if cfg.Agent != nil {
790789 i.agent = cfg.Agent
@@ -812,8 +811,8 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
812811 i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages)
813812 i.titleGenerationStarted = i.titleRealPromptSeen
814813 }
815- if cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled {
816- i.applyAutoSwarmTool(true)
814+ if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {
815+ i.applyAutoSubagentsTool(true)
817816 }
818817 return i
819818 }
@@ -1070,7 +1069,7 @@ func (i *Interactive) Run(ctx context.Context) error {
10701069 // blink inside dialogs that host their own editor (btw),
10711070 // because each frame re-emits hide-cursor + show-cursor.
10721071 //
1073- // The swarm dashboard is also animated: its rows reflect
1072+ // The subagent dashboard is also animated: its rows reflect
10741073 // background subprocesses whose activity / age change
10751074 // without any user input. Without the tick redraw the
10761075 // dashboard freezes on the snapshot taken when the user
@@ -1078,7 +1077,7 @@ func (i *Interactive) Run(ctx context.Context) error {
10781077 // of its inline editors (spawn task or prompt composer)
10791078 // is active so the cursor blink in those editors works
10801079 // the same way it does inside btw.
1081- if i.busy || i.btwDialog.Loading() || i.swarmDialog.NeedsTickRefresh() {
1080+ if i.busy || i.btwDialog.Loading() || i.subagentsDialog.NeedsTickRefresh() {
10821081 requestRedraw()
10831082 }
10841083 }
@@ -1545,8 +1544,8 @@ func (i *Interactive) redraw() {
15451544 }
15461545 i.sessionDialog.MaxRows = avail
15471546 dialog = i.sessionDialog.Render(i.cfg.Theme, cols)
1548- case i.swarmDialog.Active():
1549- dialog = i.swarmDialog.Render(i.cfg.Theme, cols)
1547+ case i.subagentsDialog.Active():
1548+ dialog = i.subagentsDialog.Render(i.cfg.Theme, cols)
15501549 case i.jumpDialog.Active():
15511550 dialog = i.jumpDialog.Render(i.cfg.Theme, cols)
15521551 case i.extPanel.Active() && (!i.confirmDialog.Active() || !i.confirmDialog.Focused()):
@@ -1770,15 +1769,15 @@ func (i *Interactive) redraw() {
17701769 bottom = append(bottom, "")
17711770 }
17721771 bottom = append(bottom, dialog...)
1773- // The swarm dashboard owns the bottom of the screen while it's
1772+ // The subagent dashboard owns the bottom of the screen while it's
17741773 // active: it has its own inline editors for spawn (`n`) and
17751774 // prompt (`p`), so the main input would be a confusing second
17761775 // caret. The suggest popup, sliding-in queue, status block, and
17771776 // main editor are all hidden underneath it. Keystrokes still
1778- // reach handleKey — it routes them to swarmDialog.HandleKey
1777+ // reach handleKey — it routes them to subagentsDialog.HandleKey
17791778 // before the editor ever sees them — so the only effect of this
17801779 // branch is visual.
1781- if !i.swarmDialog.Active() {
1780+ if !i.subagentsDialog.Active() {
17821781 bottom = append(bottom, suggest...)
17831782 bottom = append(bottom, queue...)
17841783 lineInput := inputStyle == tui.InputStyleLines
@@ -2009,8 +2008,8 @@ func (i *Interactive) redraw() {
20092008 cursorCol = c
20102009 }
20112010 }
2012- if i.swarmDialog.Active() {
2013- if r, c := i.swarmDialog.CursorPos(cols); r >= 0 {
2011+ if i.subagentsDialog.Active() {
2012+ if r, c := i.subagentsDialog.CursorPos(cols); r >= 0 {
20142013 cursorRow = dialogLead + r
20152014 cursorCol = c
20162015 } else {
@@ -2312,7 +2311,7 @@ func (i *Interactive) confirmChildActive() bool {
23122311 i.llamaDialog.Active() ||
23132312 i.rescueDialog.Active() ||
23142313 i.sessionDialog.Active() ||
2315- i.swarmDialog.Active() ||
2314+ i.subagentsDialog.Active() ||
23162315 i.jumpDialog.Active() ||
23172316 i.btwDialog.Active() ||
23182317 i.skillsDialog.Active() ||
@@ -2520,13 +2519,13 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) {
25202519 i.invalidate()
25212520 return false
25222521 }
2523- if i.swarmDialog.Active() {
2522+ if i.subagentsDialog.Active() {
25242523 if k.Kind == tui.KeyCtrlC {
2525- i.swarmDialog.Close()
2524+ i.subagentsDialog.Close()
25262525 i.invalidate()
25272526 return false
25282527 }
2529- _, msg, errMsg := i.swarmDialog.HandleKey(k)
2528+ _, msg, errMsg := i.subagentsDialog.HandleKey(k)
25302529 if msg != "" || errMsg != "" {
25312530 i.mu.Lock()
25322531 i.statusOK = msg
@@ -3505,10 +3504,10 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin
35053504 i.agent = ag
35063505 i.cfg.CWD = cwd
35073506 i.cfg.SubagentsSystemAddendum = subagentsAddendum
3508- if i.autoSwarmEnabled() {
3509- i.managedAutoSwarmAddenda = autoSwarmAddenda(i.cfg)
3507+ if i.autoSubagentsEnabled() {
3508+ i.managedAutoSubagentsAddenda = autoSubagentsAddenda(i.cfg)
35103509 } else {
3511- i.managedAutoSwarmAddenda = nil
3510+ i.managedAutoSubagentsAddenda = nil
35123511 }
35133512 i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...)
35143513 i.view.StartupContextPaths = nil
@@ -3852,15 +3851,15 @@ func (i *Interactive) openSettingsDialog() {
38523851
38533852 terminalAlerts := terminalAlertsEnabled(i.cfg.TerminalAlertsEnabled)
38543853 terminalTitles := terminalTitleEnabled(i.cfg.TerminalTitleEnabled)
3855- autoSwarm := false
3856- if i.cfg.AutoSwarmEnabled != nil {
3857- autoSwarm = *i.cfg.AutoSwarmEnabled
3854+ autoSubagents := false
3855+ if i.cfg.AutoSubagentsEnabled != nil {
3856+ autoSubagents = *i.cfg.AutoSubagentsEnabled
38583857 }
3859- autoSwarmDisabled := i.cfg.Swarm == nil
3860- autoSwarmHint := ""
3861- if autoSwarmDisabled {
3862- autoSwarm = false
3863- autoSwarmHint = "swarm supervisor not available in this mode"
3858+ autoSubagentsDisabled := i.cfg.Supervisor == nil
3859+ autoSubagentsHint := ""
3860+ if autoSubagentsDisabled {
3861+ autoSubagents = false
3862+ autoSubagentsHint = "subagent supervisor not available in this mode"
38643863 }
38653864
38663865 fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode
@@ -3982,12 +3981,12 @@ func (i *Interactive) openSettingsDialog() {
39823981 value: terminalTitles,
39833982 },
39843983 {
3985- key: "auto_swarm_enabled",
3986- label: "auto-swarm",
3984+ key: "auto_subagents_enabled",
3985+ label: "auto-subagents",
39873986 desc: "let the agent spawn background sub-agents in parallel via the subagent_spawn tool",
3988- value: autoSwarm,
3989- disabled: autoSwarmDisabled,
3990- hint: autoSwarmHint,
3987+ value: autoSubagents,
3988+ disabled: autoSubagentsDisabled,
3989+ hint: autoSubagentsHint,
39913990 },
39923991 {
39933992 key: "fast_mode",
@@ -4393,28 +4392,28 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
43934392 i.statusOK = "AI terminal titles " + onOff(value)
43944393 i.statusErr = ""
43954394 i.mu.Unlock()
4396- case "auto_swarm_enabled":
4395+ case "auto_subagents_enabled":
43974396 val := value
4398- i.cfg.AutoSwarmEnabled = &val
4397+ i.cfg.AutoSubagentsEnabled = &val
43994398 if i.cfg.SettingsStore != nil {
4400- if err := i.cfg.SettingsStore.SetAutoSwarm(value); err != nil {
4399+ if err := i.cfg.SettingsStore.SetAutoSubagents(value); err != nil {
44014400 i.mu.Lock()
44024401 i.statusErr = "settings: " + err.Error()
44034402 i.mu.Unlock()
44044403 return
44054404 }
44064405 }
4407- // Add/remove the swarm_spawn tool on the live agent so the
4406+ // Add/remove the subagent_spawn tool on the live agent so the
44084407 // model's tools[] list reflects the toggle on the next turn.
44094408 // Without this the tool stays advertised after a disable and
44104409 // the model keeps trying to call it.
4411- i.applyAutoSwarmTool(value)
4410+ i.applyAutoSubagentsTool(value)
44124411 // Also swap the system-prompt addendum in/out so the model
44134412 // knows to use the tool proactively (or stops referencing it
44144413 // after a disable).
4415- i.applyAutoSwarmSystemPrompt(value)
4414+ i.applyAutoSubagentsSystemPrompt(value)
44164415 i.mu.Lock()
4417- i.statusOK = "auto-swarm " + onOff(value)
4416+ i.statusOK = "auto-subagents " + onOff(value)
44184417 i.statusErr = ""
44194418 i.mu.Unlock()
44204419 case "fast_mode":
@@ -4440,8 +4439,8 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
44404439 if i.agent != nil {
44414440 i.agent.SetFastMode(value)
44424441 }
4443- if i.cfg.Swarm != nil {
4444- i.cfg.Swarm.SetFastMode(value)
4442+ if i.cfg.Supervisor != nil {
4443+ i.cfg.Supervisor.SetFastMode(value)
44454444 }
44464445 i.mu.Lock()
44474446 i.statusOK = "fast mode " + onOff(value)
@@ -4804,7 +4803,7 @@ func buildStudyPrompt(arg, cwd string) string {
48044803 // keeps Tab as a literal no-op.
48054804 //
48064805 // Free function (not a method) so the same logic runs against the
4807-// editor instances owned by btwDialog and swarmDialog without each
4806+// editor instances owned by btwDialog and subagentsDialog without each
48084807 // dialog needing its own copy.
48094808 func tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool {
48104809 if ed == nil {
@@ -5215,8 +5214,8 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
52155214 break
52165215 }
52175216 i.openSessionOpsDialog()
5218- case "/subagents", "/swarm":
5219- i.runSwarm(ctx, parts[1:])
5217+ case "/subagents":
5218+ i.runSubagents(ctx, parts[1:])
52205219 default:
52215220 // Last-resort fallback: try the extension manager. Built-in
52225221 // cases above always win; this branch only fires for slash
@@ -6030,7 +6029,7 @@ func (i *Interactive) swapModelUnserialized(prov, model string, builder func(str
60306029 // dynamically-registered tools need to be reattached. The apply
60316030 // helpers are no-ops when their feature is inactive, so the
60326031 // cross-provider path still works on a vanilla setup.
6033- i.applyAutoSwarmTool(i.autoSwarmEnabled())
6032+ i.applyAutoSubagentsTool(i.autoSubagentsEnabled())
60346033 i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active())
60356034 if i.cfg.PersistModel != nil {
60366035 i.cfg.PersistModel(p, md)
@@ -6064,7 +6063,7 @@ func (i *Interactive) handleAuthEvent(ev auth.Event) {
60646063 if oldAgent != nil {
60656064 _ = tools.CloseLSPManagers(oldAgent.Tools)
60666065 }
6067- i.applyAutoSwarmTool(i.autoSwarmEnabled())
6066+ i.applyAutoSubagentsTool(i.autoSubagentsEnabled())
60686067 i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active())
60696068 // Authentication can change the provider used by the live
60706069 // agent. Persist it on the active session just like /model.
@@ -7210,95 +7209,95 @@ func (a telegramSenderAdapter) Active() bool {
72107209 return a.bridge != nil && a.bridge.Active()
72117210 }
72127211
7213-// swarmWatchEntry is one tracked auto-swarm sub-agent. It records the
7212+// subagentWatchEntry is one tracked auto-subagents sub-agent. It records the
72147213 // delegated turn's outcome separately from the long-lived daemon status.
7215-type swarmWatchEntry struct {
7216- agent *swarm.Agent
7214+type subagentWatchEntry struct {
7215+ agent *subagents.Agent
72177216 task string
72187217 done bool
72197218 outcome string
72207219 err string
72217220 }
72227221
7223-// TrackSwarmAgent is the exported entry point used by the cli to
7224-// hand a freshly-spawned auto-swarm agent off to the watcher.
7225-func (i *Interactive) TrackSwarmAgent(a *swarm.Agent, task string) {
7226- i.trackSwarmAgent(a, task)
7222+// TrackSubagentWorker is the exported entry point used by the cli to
7223+// hand a freshly-spawned auto-subagents agent off to the watcher.
7224+func (i *Interactive) TrackSubagentWorker(a *subagents.Agent, task string) {
7225+ i.trackSubagentWorker(a, task)
72277226 }
72287227
7229-// trackSwarmAgent records a freshly-spawned auto-swarm agent and
7228+// trackSubagentWorker records a freshly-spawned auto-subagents agent and
72307229 // subscribes to both ways its delegated task can finish. Successful
72317230 // long-lived agents report a prompt-level turn_end and keep listening;
72327231 // startup failures and unexpected daemon exits only unblock Agent.Wait.
72337232 // Whichever signal arrives first completes the entry exactly once.
72347233 //
7235-// Wired in from cli.go via SwarmSpawnTool.OnSpawned only when auto-
7236-// swarm is enabled, so this is a no-op when the feature is off.
7237-func (i *Interactive) trackSwarmAgent(a *swarm.Agent, task string) {
7234+// Wired in from cli.go via SubagentSpawnTool.OnSpawned only when auto-
7235+// subagent is enabled, so this is a no-op when the feature is off.
7236+func (i *Interactive) trackSubagentWorker(a *subagents.Agent, task string) {
72387237 if i == nil || a == nil {
72397238 return
72407239 }
7241- entry := &swarmWatchEntry{agent: a, task: task}
7242- i.swarmWatchMu.Lock()
7243- i.swarmWatch = append(i.swarmWatch, entry)
7244- i.swarmWatchMu.Unlock()
7240+ entry := &subagentWatchEntry{agent: a, task: task}
7241+ i.subagentWatchMu.Lock()
7242+ i.subagentWatch = append(i.subagentWatch, entry)
7243+ i.subagentWatchMu.Unlock()
72457244
72467245 a.SetOnTurnEnd(func(step int, errMsg string) {
72477246 outcome := "completed"
72487247 if errMsg != "" {
72497248 outcome = "failed"
72507249 }
7251- i.completeSwarmWatchEntry(entry, outcome, errMsg)
7250+ i.completeSupervisorWatchEntry(entry, outcome, errMsg)
72527251 })
72537252 go func() {
72547253 a.Wait()
72557254 snap := a.Snapshot()
72567255 outcome := string(snap.Status)
7257- if snap.Status == swarm.StatusDone {
7256+ if snap.Status == subagents.StatusDone {
72587257 outcome = "completed"
72597258 }
7260- i.completeSwarmWatchEntry(entry, outcome, snap.Err)
7259+ i.completeSupervisorWatchEntry(entry, outcome, snap.Err)
72617260 }()
72627261 }
72637262
7264-func (i *Interactive) completeSwarmWatchEntry(entry *swarmWatchEntry, outcome, errMsg string) {
7265- i.swarmWatchMu.Lock()
7263+func (i *Interactive) completeSupervisorWatchEntry(entry *subagentWatchEntry, outcome, errMsg string) {
7264+ i.subagentWatchMu.Lock()
72667265 if entry.done {
7267- i.swarmWatchMu.Unlock()
7266+ i.subagentWatchMu.Unlock()
72687267 return
72697268 }
72707269 entry.done = true
72717270 entry.outcome = outcome
72727271 entry.err = errMsg
72737272 allDone := true
7274- for _, e := range i.swarmWatch {
7273+ for _, e := range i.subagentWatch {
72757274 if !e.done {
72767275 allDone = false
72777276 break
72787277 }
72797278 }
7280- var batch []*swarmWatchEntry
7279+ var batch []*subagentWatchEntry
72817280 if allDone {
7282- batch = i.swarmWatch
7283- i.swarmWatch = nil
7281+ batch = i.subagentWatch
7282+ i.subagentWatch = nil
72847283 }
7285- i.swarmWatchMu.Unlock()
7284+ i.subagentWatchMu.Unlock()
72867285 if len(batch) != 0 {
7287- i.flushSwarmSummary(batch)
7286+ i.flushSupervisorSummary(batch)
72887287 }
72897288 }
72907289
7291-// flushSwarmSummary composes a synthetic user turn describing every
7290+// flushSupervisorSummary composes a synthetic user turn describing every
72927291 // sub-agent's outcome and injects it via SubmitOrQueue so the main
72937292 // agent picks it up at the next safe boundary. The summary is
7294-// phrased as a system update ("Auto-swarm finished: ...") so the
7293+// phrased as a system update ("Auto-subagents finished: ...") so the
72957294 // model treats it as observed state, not as a fresh user request.
7296-func (i *Interactive) flushSwarmSummary(batch []*swarmWatchEntry) {
7295+func (i *Interactive) flushSupervisorSummary(batch []*subagentWatchEntry) {
72977296 if len(batch) == 0 {
72987297 return
72997298 }
73007299 var sb strings.Builder
7301- fmt.Fprintf(&sb, "[auto-swarm update] %d sub-agent(s) finished:\n\n", len(batch))
7300+ fmt.Fprintf(&sb, "[auto-subagents update] %d sub-agent(s) finished:\n\n", len(batch))
73027301 for idx, e := range batch {
73037302 snap := e.agent.Snapshot()
73047303 status := e.outcome
@@ -7337,20 +7336,20 @@ func truncateForSummary(s string, n int) string {
73377336 return s[:n-3] + "..."
73387337 }
73397338
7340-// autoSwarmAddenda returns the prompt blocks owned by the auto-swarm
7339+// autoSubagentsAddenda returns the prompt blocks owned by the auto-subagents
73417340 // toggle, in the same order Resolve appends them to a new agent.
7342-func autoSwarmAddenda(cfg InteractiveConfig) []string {
7341+func autoSubagentsAddenda(cfg InteractiveConfig) []string {
73437342 addenda := make([]string, 0, 2)
73447343 if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" {
73457344 addenda = append(addenda, addendum)
73467345 }
7347- if addendum := strings.TrimSpace(cfg.AutoSwarmSystemAddendum); addendum != "" {
7346+ if addendum := strings.TrimSpace(cfg.AutoSubagentsSystemAddendum); addendum != "" {
73487347 addenda = append(addenda, addendum)
73497348 }
73507349 return addenda
73517350 }
73527351
7353-func containsAutoSwarmAddendum(addenda []string, want string) bool {
7352+func containsAutoSubagentsAddendum(addenda []string, want string) bool {
73547353 for _, addendum := range addenda {
73557354 if addendum == want {
73567355 return true
@@ -7359,10 +7358,10 @@ func containsAutoSwarmAddendum(addenda []string, want string) bool {
73597358 return false
73607359 }
73617360
7362-// removeLastAutoSwarmAddendum removes one occurrence of a block known to
7363-// have been appended by auto-swarm. Resolve appends owned blocks at the
7361+// removeLastAutoSubagentsAddendum removes one occurrence of a block known to
7362+// have been appended by auto-subagents. Resolve appends owned blocks at the
73647363 // end, so removing the last occurrence preserves an identical base block.
7365-func removeLastAutoSwarmAddendum(system, addendum string) (string, bool) {
7364+func removeLastAutoSubagentsAddendum(system, addendum string) (string, bool) {
73667365 idx := strings.LastIndex(system, addendum)
73677366 if idx < 0 {
73687367 return system, false
@@ -7370,26 +7369,26 @@ func removeLastAutoSwarmAddendum(system, addendum string) (string, bool) {
73707369 return system[:idx] + system[idx+len(addendum):], true
73717370 }
73727371
7373-// applyAutoSwarmSystemPrompt appends (active=true) or strips
7374-// (active=false) the auto-swarm prompt blocks on the running agent.
7372+// applyAutoSubagentsSystemPrompt appends (active=true) or strips
7373+// (active=false) the auto-subagents prompt blocks on the running agent.
73757374 // The profile manifest and delegation guidance are managed together so
7376-// toggling auto-swarm never leaves the model with names but no tool.
7377-func (i *Interactive) applyAutoSwarmSystemPrompt(active bool) {
7375+// toggling auto-subagents never leaves the model with names but no tool.
7376+func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) {
73787377 if i.agent == nil {
73797378 return
73807379 }
7381- addenda := autoSwarmAddenda(i.cfg)
7380+ addenda := autoSubagentsAddenda(i.cfg)
73827381 if active {
73837382 sys := i.agent.System
73847383 for _, addendum := range addenda {
7385- if containsAutoSwarmAddendum(i.managedAutoSwarmAddenda, addendum) {
7384+ if containsAutoSubagentsAddendum(i.managedAutoSubagentsAddenda, addendum) {
73867385 continue
73877386 }
73887387 if sys != "" && !strings.HasSuffix(sys, "\n\n") {
73897388 sys += "\n\n"
73907389 }
73917390 sys += addendum
7392- i.managedAutoSwarmAddenda = append(i.managedAutoSwarmAddenda, addendum)
7391+ i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum)
73937392 }
73947393 i.agent.System = sys
73957394 return
@@ -7397,54 +7396,50 @@ func (i *Interactive) applyAutoSwarmSystemPrompt(active bool) {
73977396
73987397 sys := i.agent.System
73997398 changed := false
7400- for idx := len(i.managedAutoSwarmAddenda) - 1; idx >= 0; idx-- {
7399+ for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- {
74017400 var removed bool
7402- sys, removed = removeLastAutoSwarmAddendum(sys, i.managedAutoSwarmAddenda[idx])
7401+ sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx])
74037402 changed = changed || removed
74047403 }
7405- i.managedAutoSwarmAddenda = nil
7404+ i.managedAutoSubagentsAddenda = nil
74067405 if changed {
74077406 i.agent.System = strings.TrimRight(sys, "\n") + "\n"
74087407 }
74097408 }
74107409
7411-// applyAutoSwarmTool registers (active=true) or removes (active=false)
7412-// the swarm_spawn tool on the running agent so the model only sees it
7413-// when /settings -> auto-swarm is enabled. Mirrors applyTelegramTools'
7410+// applyAutoSubagentsTool registers (active=true) or removes (active=false)
7411+// the subagent_spawn tool on the running agent so the model only sees it
7412+// when /settings -> auto-subagents is enabled. Mirrors applyTelegramTools'
74147413 // snapshot+mutate pattern so extension tools and /reload-ext additions
74157414 // survive a toggle.
7416-func (i *Interactive) autoSwarmEnabled() bool {
7417- return i.cfg.AutoSwarmEnabled != nil && *i.cfg.AutoSwarmEnabled
7415+func (i *Interactive) autoSubagentsEnabled() bool {
7416+ return i.cfg.AutoSubagentsEnabled != nil && *i.cfg.AutoSubagentsEnabled
74187417 }
74197418
7420-func (i *Interactive) applyAutoSwarmTool(active bool) {
7419+func (i *Interactive) applyAutoSubagentsTool(active bool) {
74217420 if i.agent == nil {
74227421 return
74237422 }
74247423 current := i.agent.Tools
74257424 next := core.Registry{}
74267425 for name, t := range current {
7427- if name == "swarm_spawn" || name == "subagent_spawn" {
7426+ if name == "subagent_spawn" {
74287427 continue
74297428 }
74307429 next[name] = t
74317430 }
7432- toolAllowed := i.cfg.AutoSwarmToolAllowed == nil || *i.cfg.AutoSwarmToolAllowed
7433- if active && toolAllowed && i.cfg.Swarm != nil {
7434- canonical := &tools.SwarmSpawnTool{
7435- ToolName: "subagent_spawn",
7436- Swarm: i.cfg.Swarm,
7431+ toolAllowed := i.cfg.AutoSubagentsToolAllowed == nil || *i.cfg.AutoSubagentsToolAllowed
7432+ if active && toolAllowed && i.cfg.Supervisor != nil {
7433+ canonical := &tools.SubagentSpawnTool{
7434+ Supervisor: i.cfg.Supervisor,
74377435 Enabled: func() bool { return true },
74387436 DefaultModel: func() string { return i.cfg.Model },
74397437 DefaultProvider: func() string { return i.cfg.Provider },
74407438 DefaultReasoning: func() string { return i.cfg.Reasoning },
74417439 ResolveSubagent: i.cfg.ResolveSubagent,
7442- OnSpawned: i.trackSwarmAgent,
7440+ OnSpawned: i.trackSubagentWorker,
74437441 }
74447442 next[canonical.Name()] = canonical
7445- legacy := *canonical
7446- legacy.ToolName = "swarm_spawn"
7447- next[legacy.Name()] = &legacy
74487443 }
74497444 i.agent.SetTools(next)
74507445 }
M packages/agent/modes/slash_suggest.go +0 -1
@@ -63,7 +63,6 @@ var slashCatalog = []slashCommand{
6363 {Name: "/unjail", Desc: "allow tools to touch paths outside this directory"},
6464 {Name: "/skills", Desc: "list discovered skills (SKILL.md files)"},
6565 {Name: "/subagents", Desc: "supervise background subagents with lifecycle, results, and optional worktrees"},
66- {Name: "/swarm", Desc: "compatibility alias for /subagents"},
6766 {Name: "/reload-ext", Desc: "hot-reload all extensions (re-read manifests and respawn)"},
6867 {Name: "/telegram", Desc: "connect, disconnect, or show status of the telegram bridge"},
6968 {Name: "/settings", Desc: "open settings"},
M packages/agent/modes/slash_suggest_test.go +3 -3
@@ -37,10 +37,10 @@ func TestSlashSuggesterShowsLlamaOnlyWhenConfigured(t *testing.T) {
3737 }
3838 }
3939
40-func TestSlashSuggesterHasSwarm(t *testing.T) {
40+func TestSlashSuggesterHasSupervisor(t *testing.T) {
4141 s := newSlashSuggester()
42- if got := commandNames(s.matches("/sw")); !contains(got, "/swarm") {
43- t.Fatalf("/swarm missing from suggestions, got %v", got)
42+ if got := commandNames(s.matches("/su")); !contains(got, "/subagents") {
43+ t.Fatalf("/subagents missing from suggestions, got %v", got)
4444 }
4545 }
4646
R packages/agent/modes/swarm_dialog.go → packages/agent/modes/subagents_dialog.go +85 -85 renamed, 89% similar
@@ -6,13 +6,13 @@ import (
66 "strings"
77 "time"
88
9- "github.com/patriceckhart/zot/packages/agent/swarm"
9+ "github.com/patriceckhart/zot/packages/agent/subagents"
1010 "github.com/patriceckhart/zot/packages/tui"
1111 )
1212
13-// swarmDialog is the dashboard shown by /swarm (no argument) and
14-// /swarm list. It's a read-only window onto a *swarm.Swarm; the
15-// caller passes a snapshotter so the dialog never holds the swarm
13+// subagentsDialog is the dashboard shown by /subagents (no argument) and
14+// /subagents list. It's a read-only window onto a *subagents.Supervisor; the
15+// caller passes a snapshotter so the dialog never holds the subagent
1616 // pointer or its lock.
1717 //
1818 // Keys (list view):
@@ -31,16 +31,16 @@ import (
3131 // p send a follow-up prompt to this agent (inline editor)
3232 // ↑/↓/PgUp/PgDn/Home/End scroll
3333 // esc / q return to the list
34-type swarmDialog struct {
34+type subagentsDialog struct {
3535 active bool
3636 compactMode bool
3737 lineInput bool
38- snapshot func() []swarm.AgentSnapshot
38+ snapshot func() []subagents.AgentSnapshot
3939 stop func(id string) error
4040 remove func(id string) error
4141 // spawn accepts an optional model + provider override (empty
4242 // strings mean "let the child resolve its own default"). The cli
43- // adapter forwards these to swarm.Swarm.SpawnReq.
43+ // adapter forwards these to subagents.Supervisor.SpawnReq.
4444 spawn func(task, model, provider string) error
4545 // send delivers a follow-up user turn to a running agent's inbox.
4646 // Wired by Open(); when nil the inline 'p' shortcut is disabled.
@@ -50,7 +50,7 @@ type swarmDialog struct {
5050 // disabled.
5151 resume func(id string) error
5252
53- rows []swarm.AgentSnapshot
53+ rows []subagents.AgentSnapshot
5454 cursor int
5555 viewing bool // when true, show full transcript of the selected row
5656
@@ -63,7 +63,7 @@ type swarmDialog struct {
6363 transcriptSpin *spinner
6464
6565 // spawning toggles the inline "enter task" editor. While true the
66- // dialog captures keys into a real tui.Editor so swarm-new has the
66+ // dialog captures keys into a real tui.Editor so subagent-new has the
6767 // same editing, paste, drag-drop file chips, and visual style as
6868 // the default input and /btw input.
6969 spawning bool
@@ -74,7 +74,7 @@ type swarmDialog struct {
7474 // pendingModel / pendingProvider are the model + provider every
7575 // freshly-spawned agent will be pinned to. They're seeded from
7676 // the host's currently-active model (via SetCurrentModel) at the
77- // moment the user opens the dashboard, so swarm agents inherit
77+ // moment the user opens the dashboard, so subagent agents inherit
7878 // whatever the user last picked via /model. Empty means "don't
7979 // pin a model; let the child resolve its own default".
8080 pendingModel string
@@ -98,7 +98,7 @@ type swarmDialog struct {
9898
9999 // prompting toggles the inline "send prompt to selected agent"
100100 // editor. Distinct from `spawning` so we don't accidentally
101- // route a follow-up into Swarm.Spawn. The editor and its
101+ // route a follow-up into Supervisor.Spawn. The editor and its
102102 // file-suggest live in their own fields so opening the prompt
103103 // editor while a spawn editor is half-typed doesn't clobber it
104104 // (the dialog enforces one-at-a-time, but the separate state
@@ -113,7 +113,7 @@ type swarmDialog struct {
113113 promptTargetID string
114114 }
115115
116-func newSwarmDialog() *swarmDialog { return &swarmDialog{} }
116+func newSubagentsDialog() *subagentsDialog { return &subagentsDialog{} }
117117
118118 // SetCurrentModel pins the model + provider every fresh spawn will
119119 // inherit. The host wires this to the same Model / Provider the rest
@@ -121,22 +121,22 @@ func newSwarmDialog() *swarmDialog { return &swarmDialog{} }
121121 // run on whatever the user last picked via /model. Pass empty
122122 // strings to clear the override (the child then resolves its own
123123 // default the same way a bare `zot` invocation does).
124-func (d *swarmDialog) SetCompactMode(enabled bool) {
124+func (d *subagentsDialog) SetCompactMode(enabled bool) {
125125 d.compactMode = enabled
126126 }
127127
128-func (d *swarmDialog) SetLineInput(enabled bool) {
128+func (d *subagentsDialog) SetLineInput(enabled bool) {
129129 d.lineInput = enabled
130130 }
131131
132-func (d *swarmDialog) editorPrompt(th tui.Theme) string {
132+func (d *subagentsDialog) editorPrompt(th tui.Theme) string {
133133 if d.lineInput {
134134 return ""
135135 }
136136 return th.AccentBar(th.Accent)
137137 }
138138
139-func (d *swarmDialog) SetCurrentModel(model, providerID string) {
139+func (d *subagentsDialog) SetCurrentModel(model, providerID string) {
140140 d.pendingModel = model
141141 d.pendingProvider = providerID
142142 }
@@ -146,16 +146,16 @@ func (d *swarmDialog) SetCurrentModel(model, providerID string) {
146146 // the user invokes /model inside the spawn editor so the picker can
147147 // filter out unauthenticated providers, matching the global /model
148148 // behaviour.
149-func (d *swarmDialog) SetLoggedInProviders(provs []string) {
149+func (d *subagentsDialog) SetLoggedInProviders(provs []string) {
150150 d.modelPickerLoggedIn = provs
151151 }
152152
153-// Open binds the dialog to a swarm via the provided callbacks. The
153+// Open binds the dialog to a subagent via the provided callbacks. The
154154 // dialog refreshes the snapshot every Render so the dashboard reads
155155 // live state without needing a goroutine. spawn / send are optional;
156156 // when nil the corresponding inline shortcut ('n' / 'p') is disabled.
157-func (d *swarmDialog) Open(
158- snapshot func() []swarm.AgentSnapshot,
157+func (d *subagentsDialog) Open(
158+ snapshot func() []subagents.AgentSnapshot,
159159 stop func(id string) error,
160160 remove func(id string) error,
161161 spawn func(task, model, provider string) error,
@@ -194,7 +194,7 @@ func (d *swarmDialog) Open(
194194 // CursorPos returns the row/col for the terminal cursor while an
195195 // inline editor (spawn or prompt) is active so the caret blinks in
196196 // the right spot. Returns -1, -1 when no input is being captured.
197-func (d *swarmDialog) CursorPos(width int) (row, col int) {
197+func (d *subagentsDialog) CursorPos(width int) (row, col int) {
198198 if !d.Active() {
199199 return -1, -1
200200 }
@@ -264,7 +264,7 @@ func (d *swarmDialog) CursorPos(width int) (row, col int) {
264264 // belongs on when the always-on editor sits at the bottom of the
265265 // transcript view. Mirrors renderTranscript's row layout so the
266266 // terminal cursor stays glued to the editor as the body grows.
267-func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffset int) int {
267+func (d *subagentsDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffset int) int {
268268 a := d.selected()
269269 if a == nil {
270270 return -1
@@ -299,7 +299,7 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse
299299 // Full body, no scroll window — the host's outer scrollback
300300 // owns overflow now. Cursor row math just counts every
301301 // rendered line.
302- body := renderSwarmTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode)
302+ body := renderSupervisorTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode)
303303 row += len(body)
304304 }
305305
@@ -311,8 +311,8 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse
311311 return row + editorRowOffset
312312 }
313313
314-func (d *swarmDialog) Active() bool { return d != nil && d.active }
315-func (d *swarmDialog) Close() { d.active = false }
314+func (d *subagentsDialog) Active() bool { return d != nil && d.active }
315+func (d *subagentsDialog) Close() { d.active = false }
316316
317317 // NeedsTickRefresh reports whether the host's animation tick should
318318 // repaint the screen while this dialog is open. True when the
@@ -327,7 +327,7 @@ func (d *swarmDialog) Close() { d.active = false }
327327 // tick refresh. The cursor blink still works because the tick rate
328328 // is well below the terminal's blink rate, and we need the redraws
329329 // for streaming output. /btw makes the same trade-off.
330-func (d *swarmDialog) NeedsTickRefresh() bool {
330+func (d *subagentsDialog) NeedsTickRefresh() bool {
331331 if d == nil || !d.active {
332332 return false
333333 }
@@ -345,12 +345,12 @@ func (d *swarmDialog) NeedsTickRefresh() bool {
345345 }
346346
347347 // OpenViewing opens the dashboard and jumps straight to the
348-// transcript view for the agent with id agentID. Used by /swarm logs
348+// transcript view for the agent with id agentID. Used by /subagents logs
349349 // <id>. If no such agent is found the dashboard opens normally so
350350 // the user can see why (and pick from the list).
351-func (d *swarmDialog) OpenViewing(
351+func (d *subagentsDialog) OpenViewing(
352352 agentID string,
353- snapshot func() []swarm.AgentSnapshot,
353+ snapshot func() []subagents.AgentSnapshot,
354354 stop func(id string) error,
355355 remove func(id string) error,
356356 spawn func(task, model, provider string) error,
@@ -365,9 +365,9 @@ func (d *swarmDialog) OpenViewing(
365365 d.cursor = i
366366 d.viewing = true
367367 // Auto-open the inline editor for running agents so
368- // /swarm logs <id> drops the user straight into a
368+ // /subagents logs <id> drops the user straight into a
369369 // /btw-style send-ready prompt.
370- if r.Status == swarm.StatusRunning && d.send != nil {
370+ if r.Status == subagents.StatusRunning && d.send != nil {
371371 d.openPromptEditor(tui.Theme{}, r.ID)
372372 }
373373 return true
@@ -386,8 +386,8 @@ func (d *swarmDialog) OpenViewing(
386386 // so the user has context: pressing ↑/↓ they can still walk through
387387 // running agents, and the R key is a no-op there (Resume rejects
388388 // non-terminal agents with a clear error).
389-func (d *swarmDialog) OpenForResume(
390- snapshot func() []swarm.AgentSnapshot,
389+func (d *subagentsDialog) OpenForResume(
390+ snapshot func() []subagents.AgentSnapshot,
391391 stop func(id string) error,
392392 remove func(id string) error,
393393 spawn func(task, model, provider string) error,
@@ -410,14 +410,14 @@ func (d *swarmDialog) OpenForResume(
410410 }
411411
412412 // isResumable reports whether a Status is eligible for Resume. Mirror
413-// of the precondition inside Swarm.Resume; centralised so the dialog
413+// of the precondition inside Supervisor.Resume; centralised so the dialog
414414 // and the slash handler agree on what "resumable" means.
415-func isResumable(s swarm.Status) bool {
415+func isResumable(s subagents.Status) bool {
416416 switch s {
417- case swarm.StatusDetached,
418- swarm.StatusDone,
419- swarm.StatusFailed,
420- swarm.StatusKilled:
417+ case subagents.StatusDetached,
418+ subagents.StatusDone,
419+ subagents.StatusFailed,
420+ subagents.StatusKilled:
421421 return true
422422 }
423423 return false
@@ -426,8 +426,8 @@ func isResumable(s swarm.Status) bool {
426426 // promptDisabledHint returns the status-bar message shown when the
427427 // user presses 'p' on an agent that can't receive input. We branch on
428428 // status so the hint points at the right next step: resume for the
429-// states Swarm.Resume accepts; nothing useful otherwise.
430-func promptDisabledHint(s swarm.Status) string {
429+// states Supervisor.Resume accepts; nothing useful otherwise.
430+func promptDisabledHint(s subagents.Status) string {
431431 if isResumable(s) {
432432 return "prompt: agent is " + string(s) + "; press R to resume first"
433433 }
@@ -438,24 +438,24 @@ func promptDisabledHint(s swarm.Status) string {
438438 // Kill only makes sense on running / pending agents; on detached and
439439 // terminal ones it's a no-op and the user usually wants 'r' (remove)
440440 // to clear out the agent's state instead.
441-func killDisabledHint(s swarm.Status) string {
441+func killDisabledHint(s subagents.Status) string {
442442 return "kill: agent is " + string(s) + "; nothing to stop (press r to remove)"
443443 }
444444
445445 // friendlySendErr turns a raw send error into a status-bar message
446446 // the user can act on. The most common failure mode — dialling an
447447 // agent whose previous subprocess exited — surfaces as
448-// swarm.ErrNotReady; we rewrite it to mention resume instead of
448+// subagents.ErrNotReady; we rewrite it to mention resume instead of
449449 // leaking the unix-socket path. Other errors fall through verbatim
450450 // because they're rare and almost always indicate a real bug.
451451 func friendlySendErr(id string, err error) string {
452- if errors.Is(err, swarm.ErrNotReady) {
452+ if errors.Is(err, subagents.ErrNotReady) {
453453 return "send: agent " + id + " isn't accepting input (press R to resume)"
454454 }
455455 return "send: " + err.Error()
456456 }
457457
458-func (d *swarmDialog) refresh() {
458+func (d *subagentsDialog) refresh() {
459459 if d.snapshot == nil {
460460 d.rows = nil
461461 return
@@ -472,7 +472,7 @@ func (d *swarmDialog) refresh() {
472472 // HandleKey advances state. Returns (closed, statusMsg, statusErr).
473473 // statusMsg / statusErr surface in the host's status bar so the user
474474 // gets feedback on kill/remove without writing to the chat.
475-func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
475+func (d *subagentsDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
476476 if !d.Active() {
477477 return false, "", ""
478478 }
@@ -519,7 +519,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
519519 // can start typing immediately. Skipped for non-running
520520 // agents; their inbox isn't listening, so an editor
521521 // would just frustrate them with bounced sends.
522- if a := d.selected(); a != nil && a.Status == swarm.StatusRunning && d.send != nil {
522+ if a := d.selected(); a != nil && a.Status == subagents.StatusRunning && d.send != nil {
523523 d.openPromptEditor(tui.Theme{}, a.ID)
524524 }
525525 }
@@ -534,7 +534,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
534534 // agent will run against was already captured via
535535 // SetCurrentModel from the host's /model selection;
536536 // the user changes models the normal way (/model)
537- // before opening /swarm.
537+ // before opening /subagents.
538538 d.openSpawnEditor(tui.Theme{})
539539 }
540540 case 'p':
@@ -544,7 +544,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
544544 // failed / killed agent fails with a long ECONNREFUSED
545545 // or ENOENT trail that's not actionable. Tell the user
546546 // what to do instead.
547- if a.Status != swarm.StatusRunning {
547+ if a.Status != subagents.StatusRunning {
548548 return false, "", promptDisabledHint(a.Status)
549549 }
550550 d.openPromptEditor(tui.Theme{}, a.ID)
@@ -565,10 +565,10 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
565565 if a := d.selected(); a != nil && d.stop != nil {
566566 // Only running / pending agents have something to
567567 // cancel. Detached and already-terminal agents would
568- // be silent no-ops at the Swarm layer; surface a hint
568+ // be silent no-ops at the Supervisor layer; surface a hint
569569 // so the user knows kill did nothing and what to do
570570 // instead (remove via 'r').
571- if a.Status != swarm.StatusRunning && a.Status != swarm.StatusPending {
571+ if a.Status != subagents.StatusRunning && a.Status != subagents.StatusPending {
572572 return false, "", killDisabledHint(a.Status)
573573 }
574574 if err := d.stop(a.ID); err != nil {
@@ -603,14 +603,14 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
603603 // suggest, arrow keys for caret movement, etc.). Submits stay in
604604 // the view and clear the editor for the next message instead of
605605 // closing it — see handlePromptKey for the always-on behaviour.
606-func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg string) {
606+func (d *subagentsDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg string) {
607607 a := d.selected()
608608
609609 // Lazy-open the editor on first key into the transcript view, so
610610 // re-entering an agent after switching away picks up cleanly.
611611 // Skipped for non-running agents (their inbox isn't listening;
612612 // sending would just bounce).
613- if a != nil && a.Status == swarm.StatusRunning && d.send != nil && !d.prompting {
613+ if a != nil && a.Status == subagents.StatusRunning && d.send != nil && !d.prompting {
614614 d.openPromptEditor(tui.Theme{}, a.ID)
615615 }
616616
@@ -634,7 +634,7 @@ func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg stri
634634 // Everything else — typed runes, Enter, Backspace, arrow keys
635635 // for caret movement, @-file picker — is editor input. If the
636636 // agent isn't running there's no live editor; surface a hint.
637- if a == nil || a.Status != swarm.StatusRunning || d.send == nil {
637+ if a == nil || a.Status != subagents.StatusRunning || d.send == nil {
638638 if k.Kind == tui.KeyRune && a != nil {
639639 return false, "", promptDisabledHint(a.Status)
640640 }
@@ -644,10 +644,10 @@ func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg stri
644644 }
645645
646646 // openSpawnEditor enters the inline new-agent prompt. It deliberately
647-// uses tui.Editor (not a one-line string) so swarm-new supports the same
647+// uses tui.Editor (not a one-line string) so subagent-new supports the same
648648 // editing model as the normal input: multiline paste collapse, drag-drop
649649 // file/dir chips, cursor movement, and the same accent-bar prompt style.
650-func (d *swarmDialog) openSpawnEditor(th tui.Theme) {
650+func (d *subagentsDialog) openSpawnEditor(th tui.Theme) {
651651 d.spawning = true
652652 d.newTaskEd = tui.NewEditor(d.editorPrompt(th))
653653 d.fileSuggest = newFileSuggester()
@@ -662,7 +662,7 @@ func (d *swarmDialog) openSpawnEditor(th tui.Theme) {
662662 // submits "/model" from inside the spawn editor; on select we
663663 // capture the new pendingModel/Provider and reopen the spawn
664664 // editor with the previously-typed task buffer restored.
665-func (d *swarmDialog) openModelPicker() {
665+func (d *subagentsDialog) openModelPicker() {
666666 d.spawning = false
667667 d.newTaskEd = nil
668668 d.fileSuggest = nil
@@ -677,7 +677,7 @@ func (d *swarmDialog) openModelPicker() {
677677 // pendingModel + pendingProvider; Esc leaves them unchanged (the
678678 // user gets to keep whatever was inherited before they invoked
679679 // /model).
680-func (d *swarmDialog) handleModelPickerKey(k tui.Key) (closed bool, msg, errMsg string) {
680+func (d *subagentsDialog) handleModelPickerKey(k tui.Key) (closed bool, msg, errMsg string) {
681681 if d.modelPicker == nil {
682682 d.openModelPicker()
683683 }
@@ -738,7 +738,7 @@ func stripSlashModelLine(buf string) string {
738738 // openPromptEditor enters the inline "send a follow-up prompt" editor
739739 // for the agent with id targetID. Mirrors openSpawnEditor so the
740740 // editing experience (paste, @-picker, drop chips) is identical.
741-func (d *swarmDialog) openPromptEditor(th tui.Theme, targetID string) {
741+func (d *subagentsDialog) openPromptEditor(th tui.Theme, targetID string) {
742742 d.prompting = true
743743 d.promptEd = tui.NewEditor(d.editorPrompt(th))
744744 d.promptSuggest = newFileSuggester()
@@ -751,7 +751,7 @@ func (d *swarmDialog) openPromptEditor(th tui.Theme, targetID string) {
751751 // with the captured target id instead of d.spawn. Esc cancels and
752752 // returns the user to whichever view (list or transcript) was active
753753 // when 'p' was pressed.
754-func (d *swarmDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg string) {
754+func (d *subagentsDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg string) {
755755 if d.promptEd == nil {
756756 d.openPromptEditor(tui.Theme{}, d.promptTargetID)
757757 }
@@ -868,7 +868,7 @@ func (d *swarmDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg strin
868868
869869 // closePromptEditor resets all prompt-editor state. Called on Esc and
870870 // after submit so a subsequent 'p' starts from a clean slate.
871-func (d *swarmDialog) closePromptEditor() {
871+func (d *subagentsDialog) closePromptEditor() {
872872 d.prompting = false
873873 d.promptEd = nil
874874 d.promptSuggest = nil
@@ -878,7 +878,7 @@ func (d *swarmDialog) closePromptEditor() {
878878 // handleSpawnKey owns the keystrokes while the inline new-agent editor
879879 // is open. It mirrors the normal input's @ file-picker path and submit
880880 // expansion, then asks the host to spawn.
881-func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string) {
881+func (d *subagentsDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string) {
882882 if d.newTaskEd == nil {
883883 d.openSpawnEditor(tui.Theme{})
884884 }
@@ -986,7 +986,7 @@ func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string
986986 return false, "", ""
987987 }
988988
989-func (d *swarmDialog) selected() *swarm.AgentSnapshot {
989+func (d *subagentsDialog) selected() *subagents.AgentSnapshot {
990990 if d.cursor < 0 || d.cursor >= len(d.rows) {
991991 return nil
992992 }
@@ -996,7 +996,7 @@ func (d *swarmDialog) selected() *swarm.AgentSnapshot {
996996
997997 // Render returns the dialog lines. Re-reads the snapshot each call so
998998 // the dashboard updates as agents make progress.
999-func (d *swarmDialog) Render(th tui.Theme, width int) []string {
999+func (d *subagentsDialog) Render(th tui.Theme, width int) []string {
10001000 if !d.Active() {
10011001 return nil
10021002 }
@@ -1061,7 +1061,7 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string {
10611061 return out
10621062 }
10631063 if len(d.rows) == 0 {
1064- out = append(out, " "+th.FG256(th.Muted, "no agents — press n to spawn one (or /swarm new <task>)"))
1064+ out = append(out, " "+th.FG256(th.Muted, "no agents — press n to spawn one (or /subagents new <task>)"))
10651065 out = append(out, frameRule(th, width))
10661066 return out
10671067 }
@@ -1071,7 +1071,7 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string {
10711071 out = append(out, th.FG256(th.Muted, header))
10721072
10731073 for i, r := range d.rows {
1074- row := formatSwarmRow(r, width-2)
1074+ row := formatSupervisorRow(r, width-2)
10751075 if i == d.cursor {
10761076 out = append(out, th.PadHighlight(" "+row, width))
10771077 } else {
@@ -1082,14 +1082,14 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string {
10821082 return out
10831083 }
10841084
1085-func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string {
1085+func (d *subagentsDialog) renderTranscript(th tui.Theme, width int) []string {
10861086 a := d.selected()
10871087 if a == nil {
10881088 d.viewing = false
10891089 return d.Render(th, width)
10901090 }
10911091 header := []string{
1092- frameHeader(th, "swarm: "+a.ID+" (type to send, esc back)", width),
1092+ frameHeader(th, "subagent: "+a.ID+" (type to send, esc back)", width),
10931093 " " + th.FG256(th.Muted, "task: "+a.Task),
10941094 " " + th.FG256(th.Muted, "dir: "+a.Dir),
10951095 " " + th.FG256(th.Muted, fmt.Sprintf("status: %s, %s", a.Status, a.Activity)),
@@ -1141,7 +1141,7 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string {
11411141 // That gives the user the chat UX they expect (transcript grows
11421142 // with the window, arrow keys belong to the editor, no "↑ N
11431143 // more above" cliff).
1144- body := renderSwarmTranscriptBlocks(raw, th, width, d.compactMode)
1144+ body := renderSupervisorTranscriptBlocks(raw, th, width, d.compactMode)
11451145
11461146 out := header
11471147 out = append(out, body...)
@@ -1161,11 +1161,11 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string {
11611161 // row, the editor itself, another breathing row, and a single muted
11621162 // hint. For non-running agents we render a short "can't send while
11631163 // X" note instead so the user knows why typing is disabled.
1164-func (d *swarmDialog) appendTranscriptEditor(out []string, th tui.Theme, width int, a *swarm.AgentSnapshot) []string {
1164+func (d *subagentsDialog) appendTranscriptEditor(out []string, th tui.Theme, width int, a *subagents.AgentSnapshot) []string {
11651165 if a == nil {
11661166 return out
11671167 }
1168- if a.Status != swarm.StatusRunning || d.send == nil {
1168+ if a.Status != subagents.StatusRunning || d.send == nil {
11691169 out = append(out, "")
11701170 out = append(out, " "+th.FG256(th.Muted, promptDisabledHint(a.Status)))
11711171 return out
@@ -1227,11 +1227,11 @@ func (d *swarmDialog) appendTranscriptEditor(out []string, th tui.Theme, width i
12271227 // suppress the spinner so transient / unfamiliar activity strings
12281228 // still animate. Detached / done / failed / killed never animate —
12291229 // they aren't actively producing output.
1230-func agentIsBusy(a *swarm.AgentSnapshot) bool {
1230+func agentIsBusy(a *subagents.AgentSnapshot) bool {
12311231 if a == nil {
12321232 return false
12331233 }
1234- if a.Status != swarm.StatusRunning && a.Status != swarm.StatusPending {
1234+ if a.Status != subagents.StatusRunning && a.Status != subagents.StatusPending {
12351235 return false
12361236 }
12371237 switch strings.TrimSpace(a.Activity) {
@@ -1241,7 +1241,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool {
12411241 return true
12421242 }
12431243
1244-// renderSwarmTranscriptBlocks converts the agent's flat transcript
1244+// renderSupervisorTranscriptBlocks converts the agent's flat transcript
12451245 // log into a slice of display rows styled like the main chat: user
12461246 // echoes in bubble rows, assistant prose through tui.RenderMarkdown,
12471247 // stderr / error chatter tinted. Each output row is fully indented
@@ -1252,7 +1252,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool {
12521252 // whole block together — essential for markdown, which needs the
12531253 // full message body in one piece to detect fenced code spans,
12541254 // bullet lists, and so on.
1255-func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compactMode bool) []string {
1255+func renderSupervisorTranscriptBlocks(lines []string, th tui.Theme, width int, compactMode bool) []string {
12561256 type blockKind int
12571257 const (
12581258 kindAssistant blockKind = iota
@@ -1342,7 +1342,7 @@ func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compac
13421342 // the pre-built header (frame header for list view, or the agent
13431343 // metadata block for transcript view); we append onto it and close
13441344 // with the standard frame rule.
1345-func (d *swarmDialog) renderPromptEditor(th tui.Theme, width int, out []string) []string {
1345+func (d *subagentsDialog) renderPromptEditor(th tui.Theme, width int, out []string) []string {
13461346 if d.promptEd == nil {
13471347 d.openPromptEditor(th, d.promptTargetID)
13481348 }
@@ -1375,14 +1375,14 @@ func (d *swarmDialog) renderPromptEditor(th tui.Theme, width int, out []string)
13751375 return out
13761376 }
13771377
1378-// formatSwarmRow is the one-line summary shown per agent.
1378+// formatSupervisorRow is the one-line summary shown per agent.
13791379 //
13801380 // Layout (fixed-width columns, then free-form activity):
13811381 //
13821382 // STATUS ID AGE ACTIVITY
13831383 // ● run fix-login-12345 3m editing main.go
13841384 // ✓ done write-tests-67890 1h done
1385-func formatSwarmRow(r swarm.AgentSnapshot, maxWidth int) string {
1385+func formatSupervisorRow(r subagents.AgentSnapshot, maxWidth int) string {
13861386 status := statusLabel(r.Status)
13871387 if r.ProcessState != "" && r.TurnState != "" {
13881388 status = truncateLineSafe(status+" "+string(r.ProcessState)+"/"+string(r.TurnState), 18)
@@ -1414,19 +1414,19 @@ func formatSwarmRow(r swarm.AgentSnapshot, maxWidth int) string {
14141414
14151415 // statusLabel returns a short, padded status badge. Kept stable across
14161416 // runs (no colours embedded) so tests can match it deterministically.
1417-func statusLabel(s swarm.Status) string {
1417+func statusLabel(s subagents.Status) string {
14181418 switch s {
1419- case swarm.StatusPending:
1419+ case subagents.StatusPending:
14201420 return "● pend"
1421- case swarm.StatusRunning:
1421+ case subagents.StatusRunning:
14221422 return "● run"
1423- case swarm.StatusDone:
1423+ case subagents.StatusDone:
14241424 return "✓ done"
1425- case swarm.StatusFailed:
1425+ case subagents.StatusFailed:
14261426 return "✗ fail"
1427- case swarm.StatusKilled:
1427+ case subagents.StatusKilled:
14281428 return "■ kill"
1429- case swarm.StatusDetached:
1429+ case subagents.StatusDetached:
14301430 return "○ detach"
14311431 }
14321432 return string(s)
R packages/agent/modes/swarm_dialog_test.go → packages/agent/modes/subagents_dialog_test.go +170 -170 renamed, 79% similar
@@ -9,23 +9,23 @@ import (
99 "testing"
1010 "time"
1111
12- "github.com/patriceckhart/zot/packages/agent/swarm"
12+ "github.com/patriceckhart/zot/packages/agent/subagents"
1313 "github.com/patriceckhart/zot/packages/provider"
1414 "github.com/patriceckhart/zot/packages/tui"
1515 )
1616
1717 // staticSnapshots returns a deterministic snapshot slice for the
1818 // dialog to render.
19-func staticSnapshots(rows ...swarm.AgentSnapshot) func() []swarm.AgentSnapshot {
20- return func() []swarm.AgentSnapshot {
21- out := make([]swarm.AgentSnapshot, len(rows))
19+func staticSnapshots(rows ...subagents.AgentSnapshot) func() []subagents.AgentSnapshot {
20+ return func() []subagents.AgentSnapshot {
21+ out := make([]subagents.AgentSnapshot, len(rows))
2222 copy(out, rows)
2323 return out
2424 }
2525 }
2626
27-func TestSwarmDialogEmptyState(t *testing.T) {
28- d := newSwarmDialog()
27+func TestSubagentsDialogEmptyState(t *testing.T) {
28+ d := newSubagentsDialog()
2929 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")
3030 lines := d.Render(tui.Theme{}, 80)
3131 joined := strings.Join(lines, "\n")
@@ -37,13 +37,13 @@ func TestSwarmDialogEmptyState(t *testing.T) {
3737 }
3838 }
3939
40-func TestSwarmDialogRendersRows(t *testing.T) {
40+func TestSubagentsDialogRendersRows(t *testing.T) {
4141 now := time.Now()
42- rows := []swarm.AgentSnapshot{
43- {ID: "alpha-1", Task: "fix login", Status: swarm.StatusRunning, Activity: "editing file", Started: now},
44- {ID: "beta-2", Task: "write tests", Status: swarm.StatusDone, Activity: "done", Started: now},
42+ rows := []subagents.AgentSnapshot{
43+ {ID: "alpha-1", Task: "fix login", Status: subagents.StatusRunning, Activity: "editing file", Started: now},
44+ {ID: "beta-2", Task: "write tests", Status: subagents.StatusDone, Activity: "done", Started: now},
4545 }
46- d := newSwarmDialog()
46+ d := newSubagentsDialog()
4747 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
4848 out := strings.Join(d.Render(tui.Theme{}, 100), "\n")
4949 for _, want := range []string{"alpha-1", "beta-2", "editing file", "done", "STATUS", "ACTIVITY"} {
@@ -53,14 +53,14 @@ func TestSwarmDialogRendersRows(t *testing.T) {
5353 }
5454 }
5555
56-func TestSwarmDialogStopAndRemoveCallbacks(t *testing.T) {
56+func TestSubagentsDialogStopAndRemoveCallbacks(t *testing.T) {
5757 now := time.Now()
58- rows := []swarm.AgentSnapshot{
59- {ID: "alpha-1", Status: swarm.StatusRunning, Started: now},
58+ rows := []subagents.AgentSnapshot{
59+ {ID: "alpha-1", Status: subagents.StatusRunning, Started: now},
6060 }
6161 stops := 0
6262 removes := 0
63- d := newSwarmDialog()
63+ d := newSubagentsDialog()
6464 d.Open(
6565 staticSnapshots(rows...),
6666 func(id string) error { stops++; return nil },
@@ -90,12 +90,12 @@ func TestSwarmDialogStopAndRemoveCallbacks(t *testing.T) {
9090 }
9191 }
9292
93-func TestSwarmDialogEnterShowsTranscript(t *testing.T) {
93+func TestSubagentsDialogEnterShowsTranscript(t *testing.T) {
9494 now := time.Now()
95- rows := []swarm.AgentSnapshot{
96- {ID: "alpha-1", Task: "do stuff", Status: swarm.StatusDone, Tail: "line a\nline b", Started: now},
95+ rows := []subagents.AgentSnapshot{
96+ {ID: "alpha-1", Task: "do stuff", Status: subagents.StatusDone, Tail: "line a\nline b", Started: now},
9797 }
98- d := newSwarmDialog()
98+ d := newSubagentsDialog()
9999 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
100100 _ = d.Render(tui.Theme{}, 80)
101101 d.HandleKey(tui.Key{Kind: tui.KeyEnter})
@@ -113,8 +113,8 @@ func TestSwarmDialogEnterShowsTranscript(t *testing.T) {
113113 }
114114 }
115115
116-func TestSwarmDialogEscClosesList(t *testing.T) {
117- d := newSwarmDialog()
116+func TestSubagentsDialogEscClosesList(t *testing.T) {
117+ d := newSubagentsDialog()
118118 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")
119119 closed, _, _ := d.HandleKey(tui.Key{Kind: tui.KeyEsc})
120120 if !closed {
@@ -125,12 +125,12 @@ func TestSwarmDialogEscClosesList(t *testing.T) {
125125 }
126126 }
127127
128-// TestSwarmDialogSpawnFlow drives the inline n-prompt end-to-end:
128+// TestSubagentsDialogSpawnFlow drives the inline n-prompt end-to-end:
129129 // press n, type a task, press enter, observe the spawn callback
130130 // fires with the right text and the prompt closes.
131-func TestSwarmDialogSpawnFlow(t *testing.T) {
131+func TestSubagentsDialogSpawnFlow(t *testing.T) {
132132 var spawned string
133- d := newSwarmDialog()
133+ d := newSubagentsDialog()
134134 d.Open(
135135 staticSnapshots(),
136136 nil, nil,
@@ -173,13 +173,13 @@ func TestSwarmDialogSpawnFlow(t *testing.T) {
173173 }
174174 }
175175
176-func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) {
176+func TestSubagentsDialogSpawnFilePickerFlow(t *testing.T) {
177177 cwd := t.TempDir()
178178 if err := os.WriteFile(filepath.Join(cwd, "README.md"), []byte("hi"), 0o644); err != nil {
179179 t.Fatal(err)
180180 }
181181 var spawned string
182- d := newSwarmDialog()
182+ d := newSubagentsDialog()
183183 d.Open(staticSnapshots(), nil, nil, func(task, _, _ string) error { spawned = task; return nil }, nil, nil, cwd)
184184 _ = d.Render(tui.Theme{}, 80)
185185
@@ -200,7 +200,7 @@ func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) {
200200 }
201201
202202 // A second Enter submits. The chip should expand to the absolute
203- // path before the task reaches Swarm.Spawn.
203+ // path before the task reaches Supervisor.Spawn.
204204 _, msg, errMsg := d.HandleKey(tui.Key{Kind: tui.KeyEnter})
205205 if errMsg != "" {
206206 t.Fatalf("unexpected err: %s", errMsg)
@@ -214,9 +214,9 @@ func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) {
214214 }
215215 }
216216
217-func TestSwarmDialogSpawnEscCancels(t *testing.T) {
217+func TestSubagentsDialogSpawnEscCancels(t *testing.T) {
218218 called := false
219- d := newSwarmDialog()
219+ d := newSubagentsDialog()
220220 d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { called = true; return nil }, nil, nil, "")
221221 _ = d.Render(tui.Theme{}, 80)
222222
@@ -236,8 +236,8 @@ func TestSwarmDialogSpawnEscCancels(t *testing.T) {
236236 }
237237 }
238238
239-func TestSwarmDialogNoNWithoutSpawnCallback(t *testing.T) {
240- d := newSwarmDialog()
239+func TestSubagentsDialogNoNWithoutSpawnCallback(t *testing.T) {
240+ d := newSubagentsDialog()
241241 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") // spawn callback nil
242242 _ = d.Render(tui.Theme{}, 80)
243243 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'})
@@ -246,23 +246,23 @@ func TestSwarmDialogNoNWithoutSpawnCallback(t *testing.T) {
246246 }
247247 }
248248
249-// TestSwarmDialogFullTranscriptRendersEverything pins the /btw-style
249+// TestSubagentsDialogFullTranscriptRendersEverything pins the /btw-style
250250 // rendering: the dialog NEVER clips the transcript into a fixed
251251 // window with internal scroll. Every line goes into the rendered
252252 // output and the host's outer scrollback owns overflow. This is
253253 // what makes the transcript grow with the window and with the
254254 // content, instead of cutting off at row 16 with a "↑ N more above"
255255 // indicator.
256-func TestSwarmDialogFullTranscriptRendersEverything(t *testing.T) {
256+func TestSubagentsDialogFullTranscriptRendersEverything(t *testing.T) {
257257 lines := make([]string, 60)
258258 for i := range lines {
259259 lines[i] = fmt.Sprintf("line %d", i)
260260 }
261- rows := []swarm.AgentSnapshot{{
262- ID: "alpha-1", Task: "x", Status: swarm.StatusDone,
261+ rows := []subagents.AgentSnapshot{{
262+ ID: "alpha-1", Task: "x", Status: subagents.StatusDone,
263263 Lines: lines, Started: time.Now(),
264264 }}
265- d := newSwarmDialog()
265+ d := newSubagentsDialog()
266266 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
267267 _ = d.Render(tui.Theme{}, 80)
268268 d.HandleKey(tui.Key{Kind: tui.KeyEnter}) // open viewer
@@ -289,16 +289,16 @@ func TestSwarmDialogFullTranscriptRendersEverything(t *testing.T) {
289289 }
290290 }
291291
292-// TestSwarmDialogOpenViewingJumpsToAgent regression-tests /swarm logs
292+// TestSubagentsDialogOpenViewingJumpsToAgent regression-tests /subagents logs
293293 // <id>: OpenViewing should select the row whose id matches the
294294 // (possibly truncated) argument and enter the transcript view
295295 // directly without showing the list first.
296-func TestSwarmDialogOpenViewingJumpsToAgent(t *testing.T) {
297- rows := []swarm.AgentSnapshot{
296+func TestSubagentsDialogOpenViewingJumpsToAgent(t *testing.T) {
297+ rows := []subagents.AgentSnapshot{
298298 {ID: "alpha-1", Lines: []string{"hi from alpha"}, Started: time.Now()},
299299 {ID: "beta-2", Lines: []string{"hi from beta"}, Started: time.Now()},
300300 }
301- d := newSwarmDialog()
301+ d := newSubagentsDialog()
302302 ok := d.OpenViewing("beta", staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
303303 if !ok {
304304 t.Fatal("prefix match did not resolve")
@@ -312,24 +312,24 @@ func TestSwarmDialogOpenViewingJumpsToAgent(t *testing.T) {
312312 }
313313 }
314314
315-func TestSwarmDialogOpenViewingUnknownIDReturnsFalse(t *testing.T) {
316- d := newSwarmDialog()
315+func TestSubagentsDialogOpenViewingUnknownIDReturnsFalse(t *testing.T) {
316+ d := newSubagentsDialog()
317317 ok := d.OpenViewing("nope", staticSnapshots(), nil, nil, nil, nil, nil, "")
318318 if ok {
319319 t.Fatal("expected miss for unknown id")
320320 }
321321 }
322322
323-// TestSwarmDialogPromptFlowFromList drives the inline p-prompt:
323+// TestSubagentsDialogPromptFlowFromList drives the inline p-prompt:
324324 // press p on the selected agent, type a message, press enter,
325325 // and assert the send callback fired with the right id+text.
326-func TestSwarmDialogPromptFlowFromList(t *testing.T) {
327- rows := []swarm.AgentSnapshot{
328- {ID: "alpha-1", Status: swarm.StatusRunning, Started: time.Now()},
326+func TestSubagentsDialogPromptFlowFromList(t *testing.T) {
327+ rows := []subagents.AgentSnapshot{
328+ {ID: "alpha-1", Status: subagents.StatusRunning, Started: time.Now()},
329329 }
330330 var gotID, gotText string
331331 sends := 0
332- d := newSwarmDialog()
332+ d := newSubagentsDialog()
333333 d.Open(
334334 staticSnapshots(rows...),
335335 nil, nil, nil,
@@ -371,18 +371,18 @@ func TestSwarmDialogPromptFlowFromList(t *testing.T) {
371371 }
372372 }
373373
374-// TestSwarmDialogPromptFromTranscriptView pins the always-on editor
374+// TestSubagentsDialogPromptFromTranscriptView pins the always-on editor
375375 // behaviour for the transcript view: entering an agent immediately
376376 // auto-opens the inline composer (no `p` needed), typing flows
377377 // straight into it, Enter sends, and the metadata stays visible.
378378 // The transcript view is /btw-style now — you never leave the
379379 // view to send a follow-up.
380-func TestSwarmDialogPromptFromTranscriptView(t *testing.T) {
381- rows := []swarm.AgentSnapshot{
382- {ID: "beta-2", Task: "do thing", Status: swarm.StatusRunning, Started: time.Now(), Tail: "hi from beta"},
380+func TestSubagentsDialogPromptFromTranscriptView(t *testing.T) {
381+ rows := []subagents.AgentSnapshot{
382+ {ID: "beta-2", Task: "do thing", Status: subagents.StatusRunning, Started: time.Now(), Tail: "hi from beta"},
383383 }
384384 var gotID, gotText string
385- d := newSwarmDialog()
385+ d := newSubagentsDialog()
386386 d.Open(
387387 staticSnapshots(rows...),
388388 nil, nil, nil,
@@ -404,7 +404,7 @@ func TestSwarmDialogPromptFromTranscriptView(t *testing.T) {
404404 if !strings.Contains(out, "task: do thing") {
405405 t.Fatalf("transcript metadata not visible:\n%s", out)
406406 }
407- if !strings.Contains(out, "swarm: beta-2") {
407+ if !strings.Contains(out, "subagent: beta-2") {
408408 t.Fatalf("frame header missing agent id:\n%s", out)
409409 }
410410
@@ -430,12 +430,12 @@ func TestSwarmDialogPromptFromTranscriptView(t *testing.T) {
430430 }
431431 }
432432
433-// TestSwarmDialogPromptEscCancels makes sure Esc inside the prompt
433+// TestSubagentsDialogPromptEscCancels makes sure Esc inside the prompt
434434 // editor aborts without invoking the send callback.
435-func TestSwarmDialogPromptEscCancels(t *testing.T) {
436- rows := []swarm.AgentSnapshot{{ID: "gamma-3", Status: swarm.StatusRunning, Started: time.Now()}}
435+func TestSubagentsDialogPromptEscCancels(t *testing.T) {
436+ rows := []subagents.AgentSnapshot{{ID: "gamma-3", Status: subagents.StatusRunning, Started: time.Now()}}
437437 sends := 0
438- d := newSwarmDialog()
438+ d := newSubagentsDialog()
439439 d.Open(
440440 staticSnapshots(rows...),
441441 nil, nil, nil,
@@ -457,12 +457,12 @@ func TestSwarmDialogPromptEscCancels(t *testing.T) {
457457 }
458458 }
459459
460-// TestSwarmDialogPWithoutSendCallback verifies that pressing 'p'
460+// TestSubagentsDialogPWithoutSendCallback verifies that pressing 'p'
461461 // when no send callback is wired is a silent no-op (mirrors the
462462 // existing 'n' / no-spawn-callback contract).
463-func TestSwarmDialogPWithoutSendCallback(t *testing.T) {
464- rows := []swarm.AgentSnapshot{{ID: "delta-4", Status: swarm.StatusRunning, Started: time.Now()}}
465- d := newSwarmDialog()
463+func TestSubagentsDialogPWithoutSendCallback(t *testing.T) {
464+ rows := []subagents.AgentSnapshot{{ID: "delta-4", Status: subagents.StatusRunning, Started: time.Now()}}
465+ d := newSubagentsDialog()
466466 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
467467 _ = d.Render(tui.Theme{}, 80)
468468 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'})
@@ -476,13 +476,13 @@ func TestSwarmDialogPWithoutSendCallback(t *testing.T) {
476476 // dialog opens with the cursor on the first detached row (skipping
477477 // any leading running ones) so the user can press R immediately.
478478 func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) {
479- rows := []swarm.AgentSnapshot{
480- {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()},
481- {ID: "live-2", Status: swarm.StatusRunning, Started: time.Now()},
482- {ID: "detached-1", Status: swarm.StatusDetached, Started: time.Now()},
483- {ID: "detached-2", Status: swarm.StatusKilled, Started: time.Now()},
479+ rows := []subagents.AgentSnapshot{
480+ {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()},
481+ {ID: "live-2", Status: subagents.StatusRunning, Started: time.Now()},
482+ {ID: "detached-1", Status: subagents.StatusDetached, Started: time.Now()},
483+ {ID: "detached-2", Status: subagents.StatusKilled, Started: time.Now()},
484484 }
485- d := newSwarmDialog()
485+ d := newSubagentsDialog()
486486 count := d.OpenForResume(
487487 staticSnapshots(rows...),
488488 nil, nil, nil, nil,
@@ -506,10 +506,10 @@ func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) {
506506 // slash handler is responsible for surfacing the "no resumable
507507 // agents" status string — the dialog itself just opens.
508508 func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) {
509- rows := []swarm.AgentSnapshot{
510- {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()},
509+ rows := []subagents.AgentSnapshot{
510+ {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()},
511511 }
512- d := newSwarmDialog()
512+ d := newSubagentsDialog()
513513 count := d.OpenForResume(
514514 staticSnapshots(rows...),
515515 nil, nil, nil, nil,
@@ -528,15 +528,15 @@ func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) {
528528 }
529529
530530 // TestOpenForResumeREnterTriggersResume drives the full picker UX:
531-// /swarm resume → dialog opens parked on a resumable row → user
531+// /subagents resume → dialog opens parked on a resumable row → user
532532 // presses R → the resume callback fires for that exact id.
533533 func TestOpenForResumeREnterTriggersResume(t *testing.T) {
534- rows := []swarm.AgentSnapshot{
535- {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()},
536- {ID: "detached-1", Status: swarm.StatusDetached, Started: time.Now()},
534+ rows := []subagents.AgentSnapshot{
535+ {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()},
536+ {ID: "detached-1", Status: subagents.StatusDetached, Started: time.Now()},
537537 }
538538 var resumed string
539- d := newSwarmDialog()
539+ d := newSubagentsDialog()
540540 d.OpenForResume(
541541 staticSnapshots(rows...),
542542 nil, nil, nil, nil,
@@ -561,7 +561,7 @@ func TestOpenForResumeREnterTriggersResume(t *testing.T) {
561561 // the way of the inline editors (which need the cursor blink).
562562 func TestNeedsTickRefresh(t *testing.T) {
563563 // Closed.
564- d := newSwarmDialog()
564+ d := newSubagentsDialog()
565565 if d.NeedsTickRefresh() {
566566 t.Fatal("closed dialog should not request tick refresh")
567567 }
@@ -573,7 +573,7 @@ func TestNeedsTickRefresh(t *testing.T) {
573573 }
574574
575575 // Transcript view: still background-driven; tick should refresh.
576- rows := []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}}
576+ rows := []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}}
577577 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
578578 _ = d.Render(tui.Theme{}, 80)
579579 d.HandleKey(tui.Key{Kind: tui.KeyEnter})
@@ -585,7 +585,7 @@ func TestNeedsTickRefresh(t *testing.T) {
585585 }
586586
587587 // Spawn editor open: tick should NOT refresh (cursor blink).
588- d = newSwarmDialog()
588+ d = newSubagentsDialog()
589589 d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { return nil }, nil, nil, "")
590590 _ = d.Render(tui.Theme{}, 80)
591591 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'})
@@ -597,8 +597,8 @@ func TestNeedsTickRefresh(t *testing.T) {
597597 }
598598
599599 // Prompt editor open: same suppression.
600- d = newSwarmDialog()
601- rows = []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}}
600+ d = newSubagentsDialog()
601+ rows = []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}}
602602 d.Open(staticSnapshots(rows...), nil, nil, nil, func(string, string) error { return nil }, nil, "")
603603 _ = d.Render(tui.Theme{}, 80)
604604 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'})
@@ -616,12 +616,12 @@ func TestNeedsTickRefresh(t *testing.T) {
616616 // stayed at the (now-hidden) main editor row when the user
617617 // pressed 'p', so the visible caret looked wrong.
618618 func TestCursorPosPromptEditor(t *testing.T) {
619- rows := []swarm.AgentSnapshot{
620- {ID: "a", Task: "t", Status: swarm.StatusRunning, Started: time.Now()},
619+ rows := []subagents.AgentSnapshot{
620+ {ID: "a", Task: "t", Status: subagents.StatusRunning, Started: time.Now()},
621621 }
622622
623623 // From the list view.
624- d := newSwarmDialog()
624+ d := newSubagentsDialog()
625625 d.Open(staticSnapshots(rows...), nil, nil, nil,
626626 func(string, string) error { return nil }, nil, "")
627627 _ = d.Render(tui.Theme{}, 80)
@@ -635,7 +635,7 @@ func TestCursorPosPromptEditor(t *testing.T) {
635635 }
636636
637637 // From the transcript view (deeper offset due to metadata block).
638- d = newSwarmDialog()
638+ d = newSubagentsDialog()
639639 d.Open(staticSnapshots(rows...), nil, nil, nil,
640640 func(string, string) error { return nil }, nil, "")
641641 _ = d.Render(tui.Theme{}, 80)
@@ -651,7 +651,7 @@ func TestCursorPosPromptEditor(t *testing.T) {
651651 }
652652
653653 func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T) {
654- d := newSwarmDialog()
654+ d := newSubagentsDialog()
655655 d.SetCurrentModel("claude-sonnet-4-5", "anthropic")
656656 d.Open(staticSnapshots(), nil, nil, func(task, model, provider string) error { return nil }, nil, nil, "")
657657 _ = d.Render(tui.Theme{}, 80)
@@ -670,19 +670,19 @@ func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T)
670670 }
671671 }
672672
673-// TestSwarmDialogPOnNonRunningAgentShowsHint covers the regression
673+// TestSubagentsDialogPOnNonRunningAgentShowsHint covers the regression
674674 // caught by the user's screenshot: pressing 'p' on a detached agent
675675 // dialled a dead unix socket and printed the raw path as an error.
676676 // We now block the prompt editor for any non-running status and
677677 // surface a clear status-bar hint instead.
678-func TestSwarmDialogPOnNonRunningAgentShowsHint(t *testing.T) {
679- for _, st := range []swarm.Status{
680- swarm.StatusDetached, swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled,
678+func TestSubagentsDialogPOnNonRunningAgentShowsHint(t *testing.T) {
679+ for _, st := range []subagents.Status{
680+ subagents.StatusDetached, subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled,
681681 } {
682682 t.Run(string(st), func(t *testing.T) {
683- rows := []swarm.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}}
683+ rows := []subagents.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}}
684684 sends := 0
685- d := newSwarmDialog()
685+ d := newSubagentsDialog()
686686 d.Open(staticSnapshots(rows...), nil, nil, nil,
687687 func(string, string) error { sends++; return nil }, nil, "")
688688 _ = d.Render(tui.Theme{}, 80)
@@ -700,19 +700,19 @@ func TestSwarmDialogPOnNonRunningAgentShowsHint(t *testing.T) {
700700 if !strings.Contains(errMsg, string(st)) {
701701 t.Errorf("hint = %q; should mention status %q", errMsg, st)
702702 }
703- if !strings.Contains(errMsg, "resume") && st != swarm.StatusPending && st != swarm.StatusRunning {
703+ if !strings.Contains(errMsg, "resume") && st != subagents.StatusPending && st != subagents.StatusRunning {
704704 t.Errorf("hint = %q; should mention resume for resumable state", errMsg)
705705 }
706706 })
707707 }
708708 }
709709
710-// TestSwarmDialogPOnRunningAgentOpensEditor is the positive
710+// TestSubagentsDialogPOnRunningAgentOpensEditor is the positive
711711 // complement of the previous test: running agents still open the
712712 // editor normally.
713-func TestSwarmDialogPOnRunningAgentOpensEditor(t *testing.T) {
714- rows := []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}}
715- d := newSwarmDialog()
713+func TestSubagentsDialogPOnRunningAgentOpensEditor(t *testing.T) {
714+ rows := []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}}
715+ d := newSubagentsDialog()
716716 d.Open(staticSnapshots(rows...), nil, nil, nil,
717717 func(string, string) error { return nil }, nil, "")
718718 _ = d.Render(tui.Theme{}, 80)
@@ -726,11 +726,11 @@ func TestSwarmDialogPOnRunningAgentOpensEditor(t *testing.T) {
726726 // user-friendly "isn't accepting input" hint, and unknown errors
727727 // fall through verbatim.
728728 func TestFriendlySendErrRewritesNotReady(t *testing.T) {
729- hint := friendlySendErr("alpha-1", swarm.ErrNotReady)
729+ hint := friendlySendErr("alpha-1", subagents.ErrNotReady)
730730 if !strings.Contains(hint, "alpha-1") || !strings.Contains(hint, "resume") {
731731 t.Errorf("friendlySendErr(ErrNotReady) = %q", hint)
732732 }
733- if strings.Contains(hint, "swarm:") {
733+ if strings.Contains(hint, "subagent:") {
734734 t.Errorf("friendlySendErr leaked raw error text: %q", hint)
735735 }
736736
@@ -741,11 +741,11 @@ func TestFriendlySendErrRewritesNotReady(t *testing.T) {
741741 }
742742 }
743743
744-// TestRenderSwarmTranscriptBlocks_GroupsByRole asserts the flat
744+// TestRenderSupervisorTranscriptBlocks_GroupsByRole asserts the flat
745745 // transcript log is regrouped into user / assistant / stderr /
746746 // error blocks before rendering. Each output row should be styled
747747 // for its role rather than printed verbatim.
748-func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) {
748+func TestRenderSupervisorTranscriptBlocks_GroupsByRole(t *testing.T) {
749749 lines := []string{
750750 "user: hello",
751751 "hi there",
@@ -753,7 +753,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) {
753753 "stderr: warning thing",
754754 "error: boom",
755755 }
756- out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false)
756+ out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false)
757757 joined := strings.Join(out, "\n")
758758
759759 // The user echo should appear as a bubble row, which contains
@@ -780,17 +780,17 @@ func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) {
780780 }
781781 }
782782
783-// TestRenderSwarmTranscriptBlocks_MarkdownStyling proves the
783+// TestRenderSupervisorTranscriptBlocks_MarkdownStyling proves the
784784 // assistant text goes through tui.RenderMarkdown: a literal **bold**
785785 // pair should become an ANSI bold sequence and bullet "-" should
786786 // become "•".
787-func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) {
787+func TestRenderSupervisorTranscriptBlocks_MarkdownStyling(t *testing.T) {
788788 lines := []string{
789789 "Here is a list:",
790790 "- **bold** item",
791791 "- plain item",
792792 }
793- out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false)
793+ out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false)
794794 joined := strings.Join(out, "\n")
795795
796796 // ANSI bold on/off. Markdown rendering uses CSI 1 / 22.
@@ -807,11 +807,11 @@ func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) {
807807 }
808808 }
809809
810-// TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines
810+// TestRenderSupervisorTranscriptBlocks_GroupsAdjacentAssistantLines
811811 // guards the contract that consecutive assistant lines form one
812812 // markdown block. If we render each line in isolation, fenced code
813813 // (and other multi-line constructs) would never close.
814-func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) {
814+func TestRenderSupervisorTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) {
815815 lines := []string{
816816 "Some prose.",
817817 "```go",
@@ -821,7 +821,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T)
821821 "```",
822822 "Done.",
823823 }
824- out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false)
824+ out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false)
825825 joined := strings.Join(out, "\n")
826826
827827 // The literal fence markers should be consumed by the markdown
@@ -838,19 +838,19 @@ func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T)
838838 }
839839 }
840840
841-// TestSwarmDialogKOnNonRunningAgentShowsHint mirrors the prompt-
841+// TestSubagentsDialogKOnNonRunningAgentShowsHint mirrors the prompt-
842842 // disabled test: pressing 'k' on an already-terminal or detached
843-// agent now surfaces a hint instead of routing to Swarm.Stop, which
843+// agent now surfaces a hint instead of routing to Supervisor.Stop, which
844844 // is a no-op for those states (and used to segfault for detached
845845 // agents before the cancel-nil guard).
846-func TestSwarmDialogKOnNonRunningAgentShowsHint(t *testing.T) {
847- for _, st := range []swarm.Status{
848- swarm.StatusDetached, swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled,
846+func TestSubagentsDialogKOnNonRunningAgentShowsHint(t *testing.T) {
847+ for _, st := range []subagents.Status{
848+ subagents.StatusDetached, subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled,
849849 } {
850850 t.Run(string(st), func(t *testing.T) {
851- rows := []swarm.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}}
851+ rows := []subagents.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}}
852852 stops := 0
853- d := newSwarmDialog()
853+ d := newSubagentsDialog()
854854 d.Open(staticSnapshots(rows...),
855855 func(string) error { stops++; return nil },
856856 nil, nil, nil, nil, "")
@@ -866,14 +866,14 @@ func TestSwarmDialogKOnNonRunningAgentShowsHint(t *testing.T) {
866866 }
867867 }
868868
869-// TestSwarmDialogSpawnFlow_CurrentModelInherited proves the spawn
869+// TestSubagentsDialogSpawnFlow_CurrentModelInherited proves the spawn
870870 // callback receives whatever model the host pinned via
871871 // SetCurrentModel before opening the dialog. This is how /model is
872-// surfaced to swarm agents: the user picks via /model first, then
873-// /swarm n, and the new agent inherits.
874-func TestSwarmDialogSpawnFlow_CurrentModelInherited(t *testing.T) {
872+// surfaced to subagent agents: the user picks via /model first, then
873+// /subagents n, and the new agent inherits.
874+func TestSubagentsDialogSpawnFlow_CurrentModelInherited(t *testing.T) {
875875 var gotTask, gotModel, gotProv string
876- d := newSwarmDialog()
876+ d := newSubagentsDialog()
877877 d.SetCurrentModel("claude-sonnet-4-5", "anthropic")
878878 d.Open(staticSnapshots(), nil, nil,
879879 func(task, model, prov string) error {
@@ -909,14 +909,14 @@ func TestSwarmDialogSpawnFlow_CurrentModelInherited(t *testing.T) {
909909 }
910910 }
911911
912-// TestSwarmDialogSpawnEditor_SlashModelOpensPicker proves the in-
912+// TestSubagentsDialogSpawnEditor_SlashModelOpensPicker proves the in-
913913 // editor /model command pops the same model dialog the global
914914 // /model uses, that picking a model captures it as the pending
915915 // override, and that the spawn editor reopens with any previously-
916916 // typed task text intact so the user doesn't lose work.
917-func TestSwarmDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) {
917+func TestSubagentsDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) {
918918 var gotTask, gotModel, gotProv string
919- d := newSwarmDialog()
919+ d := newSubagentsDialog()
920920 d.SetCurrentModel("old-model", "old-prov")
921921 d.Open(staticSnapshots(), nil, nil,
922922 func(task, model, prov string) error {
@@ -984,12 +984,12 @@ func TestSwarmDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) {
984984 }
985985 }
986986
987-// TestSwarmDialogSpawnEditor_SlashModelEscRestoresDraft confirms
987+// TestSubagentsDialogSpawnEditor_SlashModelEscRestoresDraft confirms
988988 // pressing Esc inside the picker (no selection) reopens the spawn
989989 // editor with the previously-typed text intact AND leaves the
990990 // pending model untouched, so the user can bail out cleanly.
991-func TestSwarmDialogSpawnEditor_SlashModelEscRestoresDraft(t *testing.T) {
992- d := newSwarmDialog()
991+func TestSubagentsDialogSpawnEditor_SlashModelEscRestoresDraft(t *testing.T) {
992+ d := newSubagentsDialog()
993993 d.SetCurrentModel("keep-me", "keep-prov")
994994 d.Open(staticSnapshots(), nil, nil,
995995 func(string, string, string) error { return nil },
@@ -1041,13 +1041,13 @@ func TestStripSlashModelLine(t *testing.T) {
10411041 }
10421042 }
10431043
1044-// TestSwarmDialogSpawnFlow_NoModelInheritsChildDefault confirms that
1044+// TestSubagentsDialogSpawnFlow_NoModelInheritsChildDefault confirms that
10451045 // when the host doesn't pin a model (SetCurrentModel never called or
10461046 // called with empty strings), the spawn callback receives empty
10471047 // model/provider and the child subprocess resolves its own default.
1048-func TestSwarmDialogSpawnFlow_NoModelInheritsChildDefault(t *testing.T) {
1048+func TestSubagentsDialogSpawnFlow_NoModelInheritsChildDefault(t *testing.T) {
10491049 var gotModel, gotProv string
1050- d := newSwarmDialog()
1050+ d := newSubagentsDialog()
10511051 d.Open(staticSnapshots(), nil, nil,
10521052 func(_, model, prov string) error {
10531053 gotModel, gotProv = model, prov
@@ -1066,20 +1066,20 @@ func TestSwarmDialogSpawnFlow_NoModelInheritsChildDefault(t *testing.T) {
10661066 }
10671067 }
10681068
1069-// TestSwarmTranscriptGrowsWithNewMessages confirms the /btw-style
1069+// TestSupervisorTranscriptGrowsWithNewMessages confirms the /btw-style
10701070 // "the transcript grows with the content" property: when the agent
10711071 // reply lands in the snapshot, the next render includes it in full,
10721072 // alongside everything that was already there. Replaces the old
10731073 // followTail/scroll tests that pinned a viewport mechanic we no
10741074 // longer have.
1075-func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) {
1076- cur := []swarm.AgentSnapshot{{
1077- ID: "agent-1", Task: "x", Status: swarm.StatusRunning,
1075+func TestSupervisorTranscriptGrowsWithNewMessages(t *testing.T) {
1076+ cur := []subagents.AgentSnapshot{{
1077+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning,
10781078 Lines: []string{"old line 1", "old line 2"}, Started: time.Now(),
10791079 }}
1080- snap := func() []swarm.AgentSnapshot { return cur }
1080+ snap := func() []subagents.AgentSnapshot { return cur }
10811081
1082- d := newSwarmDialog()
1082+ d := newSubagentsDialog()
10831083 d.Open(snap, nil, nil, nil,
10841084 func(string, string) error { return nil },
10851085 nil, "")
@@ -1093,8 +1093,8 @@ func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) {
10931093
10941094 // Agent appends a fresh reply: the very next render must show
10951095 // both the new content AND keep the old content (no clipping).
1096- cur = []swarm.AgentSnapshot{{
1097- ID: "agent-1", Task: "x", Status: swarm.StatusRunning,
1096+ cur = []subagents.AgentSnapshot{{
1097+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning,
10981098 Lines: []string{"old line 1", "old line 2", "new reply A", "new reply B"},
10991099 Started: cur[0].Started,
11001100 }}
@@ -1106,19 +1106,19 @@ func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) {
11061106 }
11071107 }
11081108
1109-// TestSwarmTranscriptBtwStyleMultipleSends pins the /btw-style flow
1109+// TestSupervisorTranscriptBtwStyleMultipleSends pins the /btw-style flow
11101110 // for the transcript view: after one Enter-submit the editor stays
11111111 // mounted and clears, and a second message can be composed and sent
11121112 // without any modal toggle. Regresses the previous design where
11131113 // every send closed the prompt and forced the user to press `p`
11141114 // again.
1115-func TestSwarmTranscriptBtwStyleMultipleSends(t *testing.T) {
1116- rows := []swarm.AgentSnapshot{{
1117- ID: "agent-1", Task: "x", Status: swarm.StatusRunning, Started: time.Now(),
1115+func TestSupervisorTranscriptBtwStyleMultipleSends(t *testing.T) {
1116+ rows := []subagents.AgentSnapshot{{
1117+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning, Started: time.Now(),
11181118 Lines: []string{"warm-up"},
11191119 }}
11201120 var sent []string
1121- d := newSwarmDialog()
1121+ d := newSubagentsDialog()
11221122 d.Open(staticSnapshots(rows...), nil, nil, nil,
11231123 func(_, text string) error { sent = append(sent, text); return nil },
11241124 nil, "")
@@ -1145,18 +1145,18 @@ func TestSwarmTranscriptBtwStyleMultipleSends(t *testing.T) {
11451145 }
11461146 }
11471147
1148-// TestSwarmTranscriptEscClosesView pins the Esc semantics for the
1148+// TestSupervisorTranscriptEscClosesView pins the Esc semantics for the
11491149 // /btw-style transcript view: a single Esc closes the whole view
11501150 // (back to the dashboard list), regardless of whether the editor
11511151 // is currently composing. Without this routing the editor's own
11521152 // Esc handler would just clear the modal and leave the user stuck
11531153 // inside an empty transcript view.
1154-func TestSwarmTranscriptEscClosesView(t *testing.T) {
1155- rows := []swarm.AgentSnapshot{{
1156- ID: "agent-1", Task: "x", Status: swarm.StatusRunning, Started: time.Now(),
1154+func TestSupervisorTranscriptEscClosesView(t *testing.T) {
1155+ rows := []subagents.AgentSnapshot{{
1156+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning, Started: time.Now(),
11571157 Lines: []string{"hi"},
11581158 }}
1159- d := newSwarmDialog()
1159+ d := newSubagentsDialog()
11601160 d.Open(staticSnapshots(rows...), nil, nil, nil,
11611161 func(string, string) error { return nil },
11621162 nil, "")
@@ -1175,17 +1175,17 @@ func TestSwarmTranscriptEscClosesView(t *testing.T) {
11751175 }
11761176 }
11771177
1178-// TestSwarmTranscriptNonRunningAgentNoEditor pins the safety net:
1178+// TestSupervisorTranscriptNonRunningAgentNoEditor pins the safety net:
11791179 // transcripts for done / failed / killed agents must NOT auto-open
11801180 // an editor (the agent's inbox isn't listening, so a send would
11811181 // just bounce). The view shows the transcript plus a hint about
11821182 // resuming instead.
1183-func TestSwarmTranscriptNonRunningAgentNoEditor(t *testing.T) {
1184- rows := []swarm.AgentSnapshot{{
1185- ID: "agent-1", Task: "x", Status: swarm.StatusDone, Started: time.Now(),
1183+func TestSupervisorTranscriptNonRunningAgentNoEditor(t *testing.T) {
1184+ rows := []subagents.AgentSnapshot{{
1185+ ID: "agent-1", Task: "x", Status: subagents.StatusDone, Started: time.Now(),
11861186 Lines: []string{"all done"},
11871187 }}
1188- d := newSwarmDialog()
1188+ d := newSubagentsDialog()
11891189 d.Open(staticSnapshots(rows...), nil, nil, nil,
11901190 func(string, string) error { return nil },
11911191 nil, "")
@@ -1200,19 +1200,19 @@ func TestSwarmTranscriptNonRunningAgentNoEditor(t *testing.T) {
12001200 }
12011201 }
12021202
1203-// TestSwarmTranscriptTickRefreshWhileEditing pins the streaming
1203+// TestSupervisorTranscriptTickRefreshWhileEditing pins the streaming
12041204 // fix: with the always-on editor in transcript view, the host's
12051205 // animation tick MUST still call us so streaming agent output
12061206 // renders. Without this, sending a message and watching the reply
12071207 // land silently failed because the prompting=true branch was
12081208 // suppressing redraws (like the modal editors do).
1209-func TestSwarmTranscriptTickRefreshWhileEditing(t *testing.T) {
1210- rows := []swarm.AgentSnapshot{{
1211- ID: "agent-1", Task: "x", Status: swarm.StatusRunning,
1209+func TestSupervisorTranscriptTickRefreshWhileEditing(t *testing.T) {
1210+ rows := []subagents.AgentSnapshot{{
1211+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning,
12121212 Activity: "thinking", Started: time.Now(),
12131213 Lines: []string{"hello"},
12141214 }}
1215- d := newSwarmDialog()
1215+ d := newSubagentsDialog()
12161216 d.Open(staticSnapshots(rows...), nil, nil, nil,
12171217 func(string, string) error { return nil },
12181218 nil, "")
@@ -1227,15 +1227,15 @@ func TestSwarmTranscriptTickRefreshWhileEditing(t *testing.T) {
12271227 }
12281228 }
12291229
1230-// TestSwarmTranscriptTickRefreshModalSuppressed pins the inverse:
1230+// TestSupervisorTranscriptTickRefreshModalSuppressed pins the inverse:
12311231 // the list-view `p` modal (prompting without viewing) still
12321232 // suppresses tick like the other modal editors, so the cursor
12331233 // blink works cleanly while composing a one-off prompt.
1234-func TestSwarmTranscriptTickRefreshModalSuppressed(t *testing.T) {
1235- rows := []swarm.AgentSnapshot{{
1236- ID: "agent-1", Status: swarm.StatusRunning, Started: time.Now(),
1234+func TestSupervisorTranscriptTickRefreshModalSuppressed(t *testing.T) {
1235+ rows := []subagents.AgentSnapshot{{
1236+ ID: "agent-1", Status: subagents.StatusRunning, Started: time.Now(),
12371237 }}
1238- d := newSwarmDialog()
1238+ d := newSubagentsDialog()
12391239 d.Open(staticSnapshots(rows...), nil, nil, nil,
12401240 func(string, string) error { return nil },
12411241 nil, "")
@@ -1250,18 +1250,18 @@ func TestSwarmTranscriptTickRefreshModalSuppressed(t *testing.T) {
12501250 }
12511251 }
12521252
1253-// TestSwarmTranscriptBusySpinnerRenders proves the inline spinner
1253+// TestSupervisorTranscriptBusySpinnerRenders proves the inline spinner
12541254 // appears above the always-on editor while the agent is busy and
12551255 // disappears when it goes idle. This is the /btw-style "something
12561256 // is happening" feedback the user was missing.
1257-func TestSwarmTranscriptBusySpinnerRenders(t *testing.T) {
1258- cur := []swarm.AgentSnapshot{{
1259- ID: "agent-1", Task: "x", Status: swarm.StatusRunning,
1257+func TestSupervisorTranscriptBusySpinnerRenders(t *testing.T) {
1258+ cur := []subagents.AgentSnapshot{{
1259+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning,
12601260 Activity: "thinking", Started: time.Now(),
12611261 Lines: []string{"hello"},
12621262 }}
1263- snap := func() []swarm.AgentSnapshot { return cur }
1264- d := newSwarmDialog()
1263+ snap := func() []subagents.AgentSnapshot { return cur }
1264+ d := newSubagentsDialog()
12651265 d.Open(snap, nil, nil, nil,
12661266 func(string, string) error { return nil },
12671267 nil, "")
@@ -1289,9 +1289,9 @@ func TestSwarmTranscriptBusySpinnerRenders(t *testing.T) {
12891289 }
12901290
12911291 func TestStatusLabelCoversAll(t *testing.T) {
1292- for _, s := range []swarm.Status{
1293- swarm.StatusPending, swarm.StatusRunning,
1294- swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled,
1292+ for _, s := range []subagents.Status{
1293+ subagents.StatusPending, subagents.StatusRunning,
1294+ subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled,
12951295 } {
12961296 if statusLabel(s) == "" {
12971297 t.Fatalf("no label for %s", s)
R packages/agent/modes/swarm_slash.go → packages/agent/modes/subagents_slash.go +94 -94 renamed, 60% similar
@@ -5,30 +5,30 @@ import (
55 "fmt"
66 "strings"
77
8- "github.com/patriceckhart/zot/packages/agent/swarm"
8+ "github.com/patriceckhart/zot/packages/agent/subagents"
99 "github.com/patriceckhart/zot/packages/provider"
1010 "github.com/patriceckhart/zot/packages/tui"
1111 )
1212
13-// runSwarm dispatches /swarm subcommands. Layout:
13+// runSubagents dispatches /subagents subcommands. Layout:
1414 //
15-// /swarm -> open the dashboard
16-// /swarm list -> open the dashboard
17-// /swarm new [--agent A] [--model M] [--provider P] [--reasoning L] <task...>
15+// /subagents -> open the dashboard
16+// /subagents list -> open the dashboard
17+// /subagents new [--agent A] [--model M] [--provider P] [--reasoning L] <task...>
1818 // -> spawn an agent with optional profile/model overrides
19-// /swarm kill <id> -> stop a running agent
20-// /swarm remove <id> -> tear down a terminated agent
21-// /swarm logs <id> -> open the scrollable transcript view
22-// /swarm send <id> <text...> -> send a follow-up user turn to <id>
23-// /swarm resume [id] -> resume an agent (omit id to pick from a list)
24-// /swarm attach <id> -> (planned) drop into the agent's TUI
19+// /subagents kill <id> -> stop a running agent
20+// /subagents remove <id> -> tear down a terminated agent
21+// /subagents logs <id> -> open the scrollable transcript view
22+// /subagents send <id> <text...> -> send a follow-up user turn to <id>
23+// /subagents resume [id] -> resume an agent (omit id to pick from a list)
24+// /subagents attach <id> -> (planned) drop into the agent's TUI
2525 //
26-// When cfg.Swarm is nil the command tells the user the feature is
26+// When cfg.Supervisor is nil the command tells the user the feature is
2727 // disabled instead of pretending to work.
28-func (i *Interactive) runSwarm(ctx context.Context, args []string) {
29- if i.cfg.Swarm == nil {
28+func (i *Interactive) runSubagents(ctx context.Context, args []string) {
29+ if i.cfg.Supervisor == nil {
3030 i.mu.Lock()
31- i.statusErr = "swarm is disabled in this build"
31+ i.statusErr = "subagent is disabled in this build"
3232 i.statusOK = ""
3333 i.mu.Unlock()
3434 return
@@ -39,51 +39,51 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
3939 if len(args) > 0 {
4040 sub = strings.ToLower(args[0])
4141 // Guard the args[1:] reslice: when only the subcommand was
42- // typed (e.g. bare "/swarm new"), args has length 1 and the
42+ // typed (e.g. bare "/subagents new"), args has length 1 and the
4343 // naive args[1:] is fine, but when args is empty (bare
44- // "/swarm") the reslice is [1:0] and panics. The len>0 branch
44+ // "/subagents") the reslice is [1:0] and panics. The len>0 branch
4545 // here keeps both cases safe.
4646 if len(args) > 1 {
4747 rest = strings.TrimSpace(strings.Join(args[1:], " "))
4848 }
4949 }
5050
51- // spawnAdapter, sendAdapter, resumeAdapter wrap the Swarm methods
51+ // spawnAdapter, sendAdapter, resumeAdapter wrap the Supervisor methods
5252 // in the signatures the dialog expects. Defined once here so the
5353 // three Open()-shaped entry points (list, logs/view-jump, resume)
5454 // feed the dialog identical callbacks.
5555 spawnAdapter := func(task, model, provider string) error {
56- _, err := i.cfg.Swarm.SpawnReq(ctx, swarm.SpawnRequest{
56+ _, err := i.cfg.Supervisor.SpawnReq(ctx, subagents.SpawnRequest{
5757 Task: task, Model: model, Provider: provider, Reasoning: i.cfg.Reasoning,
5858 })
5959 return err
6060 }
6161 sendAdapter := func(id, text string) error {
62- return i.cfg.Swarm.SendUserTurn(id, text)
62+ return i.cfg.Supervisor.SendUserTurn(id, text)
6363 }
6464 resumeAdapter := func(id string) error {
65- _, err := i.cfg.Swarm.ResumeSession(ctx, id)
65+ _, err := i.cfg.Supervisor.ResumeSession(ctx, id)
6666 return err
6767 }
6868
6969 // Pin every fresh spawn to whatever the host's /model selection
70- // is right now. This is captured at /swarm time — if the user
71- // wants a different model for the next swarm agent, they pick it
70+ // is right now. This is captured at /subagents time — if the user
71+ // wants a different model for the next subagent agent, they pick it
7272 // via /model first (globally), or, while inside the spawn
7373 // editor, by typing /model on its own line to pop the picker.
74- i.swarmDialog.SetCompactMode(i.compactModeEnabled())
75- i.swarmDialog.SetLineInput(tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines)
76- i.swarmDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider)
74+ i.subagentsDialog.SetCompactMode(i.compactModeEnabled())
75+ i.subagentsDialog.SetLineInput(tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines)
76+ i.subagentsDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider)
7777 if i.cfg.LoggedInProviders != nil {
78- i.swarmDialog.SetLoggedInProviders(i.cfg.LoggedInProviders())
78+ i.subagentsDialog.SetLoggedInProviders(i.cfg.LoggedInProviders())
7979 }
8080
8181 switch sub {
8282 case "", "list", "ls", "ps":
83- i.swarmDialog.Open(
84- i.cfg.Swarm.SnapshotAll,
85- i.cfg.Swarm.Stop,
86- i.cfg.Swarm.Remove,
83+ i.subagentsDialog.Open(
84+ i.cfg.Supervisor.SnapshotAll,
85+ i.cfg.Supervisor.Stop,
86+ i.cfg.Supervisor.Remove,
8787 spawnAdapter,
8888 sendAdapter,
8989 resumeAdapter,
@@ -91,7 +91,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
9191 )
9292 case "new", "spawn":
9393 if rest == "" {
94- i.swarmStatus("", "/swarm new <task>: missing task")
94+ i.subagentsStatus("", "/subagents new <task>: missing task")
9595 return
9696 }
9797 // Permit profile/model/provider/reasoning flags before the task
@@ -102,21 +102,21 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
102102 var profileTools []string
103103 var fastModeOverride *bool
104104 if task == "" {
105- i.swarmStatus("", "/swarm new: missing task (after any spawn flags)")
105+ i.subagentsStatus("", "/subagents new: missing task (after any spawn flags)")
106106 return
107107 }
108108 if subagent != "" {
109109 if i.cfg.ResolveSubagent == nil {
110- i.swarmStatus("", "spawn: named subagent profiles are unavailable")
110+ i.subagentsStatus("", "spawn: named subagent profiles are unavailable")
111111 return
112112 }
113113 profile, err := i.cfg.ResolveSubagent(subagent)
114114 if err != nil {
115- i.swarmStatus("", "spawn: "+err.Error())
115+ i.subagentsStatus("", "spawn: "+err.Error())
116116 return
117117 }
118118 if profile == nil {
119- i.swarmStatus("", "spawn: unknown subagent profile "+subagent)
119+ i.subagentsStatus("", "spawn: unknown subagent profile "+subagent)
120120 return
121121 }
122122 subagent = profile.Name
@@ -145,18 +145,18 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
145145 if reasoning != "" {
146146 normalizedReasoning, err := normalizeSpawnReasoning(reasoning)
147147 if err != nil {
148- i.swarmStatus("", "spawn: "+err.Error())
148+ i.subagentsStatus("", "spawn: "+err.Error())
149149 return
150150 }
151151 reasoning = normalizedReasoning
152152 }
153- a, err := i.cfg.Swarm.SpawnReq(ctx, swarm.SpawnRequest{
153+ a, err := i.cfg.Supervisor.SpawnReq(ctx, subagents.SpawnRequest{
154154 Task: task, Model: model, Provider: provider, Reasoning: reasoning,
155155 FastMode: fastModeOverride, Subagent: subagent,
156156 Tools: profileTools,
157157 })
158158 if err != nil {
159- i.swarmStatus("", "spawn: "+err.Error())
159+ i.subagentsStatus("", "spawn: "+err.Error())
160160 return
161161 }
162162 status := "spawned " + a.ID
@@ -165,91 +165,91 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
165165 } else if model != "" {
166166 status += " (model " + model + ")"
167167 }
168- i.swarmStatus(status, "")
168+ i.subagentsStatus(status, "")
169169 case "kill", "stop":
170170 if rest == "" {
171- i.swarmStatus("", "/subagents kill <id>: missing id")
171+ i.subagentsStatus("", "/subagents kill <id>: missing id")
172172 return
173173 }
174- if err := i.cfg.Swarm.Stop(rest); err != nil {
175- i.swarmStatus("", "kill: "+err.Error())
174+ if err := i.cfg.Supervisor.Stop(rest); err != nil {
175+ i.subagentsStatus("", "kill: "+err.Error())
176176 return
177177 }
178- i.swarmStatus("stopped "+rest, "")
178+ i.subagentsStatus("stopped "+rest, "")
179179 case "cancel":
180180 if rest == "" {
181- i.swarmStatus("", "/subagents cancel <id>: missing id")
181+ i.subagentsStatus("", "/subagents cancel <id>: missing id")
182182 return
183183 }
184- if err := i.cfg.Swarm.CancelTurn(rest); err != nil {
185- i.swarmStatus("", "cancel: "+err.Error())
184+ if err := i.cfg.Supervisor.CancelTurn(rest); err != nil {
185+ i.subagentsStatus("", "cancel: "+err.Error())
186186 return
187187 }
188- i.swarmStatus("canceling turn "+rest, "")
188+ i.subagentsStatus("canceling turn "+rest, "")
189189 case "remove", "rm":
190190 if rest == "" {
191- i.swarmStatus("", "/swarm remove <id>: missing id")
191+ i.subagentsStatus("", "/subagents remove <id>: missing id")
192192 return
193193 }
194- if err := i.cfg.Swarm.Remove(rest); err != nil {
195- i.swarmStatus("", "remove: "+err.Error())
194+ if err := i.cfg.Supervisor.Remove(rest); err != nil {
195+ i.subagentsStatus("", "remove: "+err.Error())
196196 return
197197 }
198- i.swarmStatus("removed "+rest, "")
198+ i.subagentsStatus("removed "+rest, "")
199199 case "logs", "log", "view":
200200 if rest == "" {
201- i.swarmStatus("", "/swarm logs <id>: missing id")
201+ i.subagentsStatus("", "/subagents logs <id>: missing id")
202202 return
203203 }
204- ok := i.swarmDialog.OpenViewing(
204+ ok := i.subagentsDialog.OpenViewing(
205205 rest,
206- i.cfg.Swarm.SnapshotAll,
207- i.cfg.Swarm.Stop,
208- i.cfg.Swarm.Remove,
206+ i.cfg.Supervisor.SnapshotAll,
207+ i.cfg.Supervisor.Stop,
208+ i.cfg.Supervisor.Remove,
209209 spawnAdapter,
210210 sendAdapter,
211211 resumeAdapter,
212212 i.cfg.CWD,
213213 )
214214 if !ok {
215- i.swarmStatus("", "/swarm logs: no agent matching "+rest)
215+ i.subagentsStatus("", "/subagents logs: no agent matching "+rest)
216216 }
217217 case "result", "inspect":
218218 if rest == "" {
219- i.swarmStatus("", "/subagents result <id>: missing id")
219+ i.subagentsStatus("", "/subagents result <id>: missing id")
220220 return
221221 }
222- result, err := i.cfg.Swarm.ReadResult(rest)
222+ result, err := i.cfg.Supervisor.ReadResult(rest)
223223 if err != nil {
224- i.swarmStatus("", "result: "+err.Error())
224+ i.subagentsStatus("", "result: "+err.Error())
225225 return
226226 }
227- status := fmt.Sprintf("%s result %s", result.Status, i.cfg.Swarm.ResultReference(rest))
227+ status := fmt.Sprintf("%s result %s", result.Status, i.cfg.Supervisor.ResultReference(rest))
228228 if result.Summary != "" {
229229 status += ": " + truncateStatus(result.Summary, 160)
230230 }
231- i.swarmStatus(status, "")
231+ i.subagentsStatus(status, "")
232232 case "wait":
233233 if rest == "" {
234- i.swarmStatus("", "/subagents wait <id>: missing id")
234+ i.subagentsStatus("", "/subagents wait <id>: missing id")
235235 return
236236 }
237- a := i.cfg.Swarm.Get(rest)
237+ a := i.cfg.Supervisor.Get(rest)
238238 if a == nil {
239- i.swarmStatus("", "wait: no such agent "+rest)
239+ i.subagentsStatus("", "wait: no such agent "+rest)
240240 return
241241 }
242242 go func() {
243243 a.Wait()
244- i.swarmStatus("completed "+a.ID, "")
244+ i.subagentsStatus("completed "+a.ID, "")
245245 }()
246- i.swarmStatus("waiting for "+a.ID, "")
246+ i.subagentsStatus("waiting for "+a.ID, "")
247247 case "resume-session", "resume", "reattach", "reopen":
248248 if rest == "" {
249- count := i.swarmDialog.OpenForResume(
250- i.cfg.Swarm.SnapshotAll,
251- i.cfg.Swarm.Stop,
252- i.cfg.Swarm.Remove,
249+ count := i.subagentsDialog.OpenForResume(
250+ i.cfg.Supervisor.SnapshotAll,
251+ i.cfg.Supervisor.Stop,
252+ i.cfg.Supervisor.Remove,
253253 spawnAdapter,
254254 sendAdapter,
255255 resumeAdapter,
@@ -257,61 +257,61 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
257257 )
258258 switch count {
259259 case 0:
260- i.swarmStatus("", "/subagents resume-session: no resumable agents")
260+ i.subagentsStatus("", "/subagents resume-session: no resumable agents")
261261 case 1:
262- i.swarmStatus("1 resumable agent, press R to resume", "")
262+ i.subagentsStatus("1 resumable agent, press R to resume", "")
263263 default:
264- i.swarmStatus(fmt.Sprintf("%d resumable agents, ↑/↓ to pick, R to resume", count), "")
264+ i.subagentsStatus(fmt.Sprintf("%d resumable agents, ↑/↓ to pick, R to resume", count), "")
265265 }
266266 return
267267 }
268- a, err := i.cfg.Swarm.ResumeSession(ctx, rest)
268+ a, err := i.cfg.Supervisor.ResumeSession(ctx, rest)
269269 if err != nil {
270- i.swarmStatus("", "resume-session: "+err.Error())
270+ i.subagentsStatus("", "resume-session: "+err.Error())
271271 return
272272 }
273- i.swarmStatus("resumed "+a.ID, "")
273+ i.subagentsStatus("resumed "+a.ID, "")
274274 case "restart-task", "restart":
275275 if rest == "" {
276- i.swarmStatus("", "/subagents restart-task <id>: missing id")
276+ i.subagentsStatus("", "/subagents restart-task <id>: missing id")
277277 return
278278 }
279- a, err := i.cfg.Swarm.RestartTask(ctx, rest)
279+ a, err := i.cfg.Supervisor.RestartTask(ctx, rest)
280280 if err != nil {
281- i.swarmStatus("", "restart-task: "+err.Error())
281+ i.subagentsStatus("", "restart-task: "+err.Error())
282282 return
283283 }
284- i.swarmStatus("restarted task "+a.ID, "")
284+ i.subagentsStatus("restarted task "+a.ID, "")
285285 case "send", "prompt", "msg":
286- // /swarm send <id> <text...> is the non-interactive
286+ // /subagents send <id> <text...> is the non-interactive
287287 // counterpart of pressing 'p' in the dashboard. We split the
288288 // joined `rest` ourselves rather than reusing the dispatcher's
289289 // already-fielded args[] because the text may contain spaces
290290 // the user expects to be preserved verbatim.
291291 id, text := splitIDAndRest(rest)
292292 if id == "" {
293- i.swarmStatus("", "/swarm send <id> <text>: missing id")
293+ i.subagentsStatus("", "/subagents send <id> <text>: missing id")
294294 return
295295 }
296296 if text == "" {
297- i.swarmStatus("", "/swarm send <id> <text>: missing text")
297+ i.subagentsStatus("", "/subagents send <id> <text>: missing text")
298298 return
299299 }
300- if err := i.cfg.Swarm.SendUserTurn(id, text); err != nil {
301- i.swarmStatus("", friendlySendErr(id, err))
300+ if err := i.cfg.Supervisor.SendUserTurn(id, text); err != nil {
301+ i.subagentsStatus("", friendlySendErr(id, err))
302302 return
303303 }
304- i.swarmStatus("sent to "+id, "")
304+ i.subagentsStatus("sent to "+id, "")
305305 case "attach":
306306 // PTY-reparenting is a significant chunk of work I haven't
307307 // landed yet (see the design sketch). Recognise the name so
308- // /swarm attach doesn't fall through to the generic "unknown
308+ // /subagents attach doesn't fall through to the generic "unknown
309309 // subcommand" path — that error message is misleading because
310310 // it makes attach sound like a typo instead of a planned
311- // feature. Point the user at /swarm logs in the meantime.
312- i.swarmStatus("", "/swarm attach: not implemented yet (needs PTY reparenting). Use /swarm logs "+firstWord(rest)+" to watch its transcript.")
311+ // feature. Point the user at /subagents logs in the meantime.
312+ i.subagentsStatus("", "/subagents attach: not implemented yet (needs PTY reparenting). Use /subagents logs "+firstWord(rest)+" to watch its transcript.")
313313 default:
314- i.swarmStatus("", "/subagents: unknown subcommand "+sub+" (try list / new / kill / remove / logs / send / result / resume-session / restart-task)")
314+ i.subagentsStatus("", "/subagents: unknown subcommand "+sub+" (try list / new / kill / remove / logs / send / result / resume-session / restart-task)")
315315 }
316316 }
317317
@@ -418,7 +418,7 @@ func splitIDAndRest(s string) (id, text string) {
418418 }
419419
420420 // firstWord returns the first whitespace-separated token of s, or
421-// "<id>" when s is empty. Used to keep the "/swarm attach" hint
421+// "<id>" when s is empty. Used to keep the "/subagents attach" hint
422422 // readable even when the user typed no argument.
423423 func firstWord(s string) string {
424424 s = strings.TrimSpace(s)
@@ -442,7 +442,7 @@ func truncateStatus(value string, max int) string {
442442 return string([]rune(value)[:max-3]) + "..."
443443 }
444444
445-func (i *Interactive) swarmStatus(ok, errMsg string) {
445+func (i *Interactive) subagentsStatus(ok, errMsg string) {
446446 i.mu.Lock()
447447 i.statusOK = ok
448448 i.statusErr = errMsg
R packages/agent/modes/swarm_slash_test.go → packages/agent/modes/subagents_slash_test.go +73 -64 renamed, 59% similar
@@ -8,54 +8,53 @@ import (
88 "time"
99
1010 "github.com/patriceckhart/zot/packages/agent/subagents"
11- "github.com/patriceckhart/zot/packages/agent/swarm"
1211 )
1312
14-// newInteractiveForSwarmTest builds the minimal Interactive scaffolding
15-// runSwarm needs. It does NOT call NewInteractive (which would pull in
16-// the whole TUI); the runSwarm method only touches cfg.Swarm, the
17-// status mutex, and the swarm dialog, so we hand-build those.
18-func newInteractiveForSwarmTest(t *testing.T) (*Interactive, *swarm.Swarm) {
13+// newInteractiveForSupervisorTest builds the minimal Interactive scaffolding
14+// runSubagents needs. It does NOT call NewInteractive (which would pull in
15+// the whole TUI); the runSubagents method only touches cfg.Supervisor, the
16+// status mutex, and the subagent dialog, so we hand-build those.
17+func newInteractiveForSupervisorTest(t *testing.T) (*Interactive, *subagents.Supervisor) {
1918 t.Helper()
2019 root := t.TempDir()
21- f := swarm.New(swarm.Config{
20+ f := subagents.New(subagents.Config{
2221 Root: root,
2322 RepoRoot: root,
24- NewRunner: func(a *swarm.Agent) swarm.Runner {
25- return swarm.RunnerFunc(func(ctx context.Context, sink swarm.Sink) error {
23+ NewRunner: func(a *subagents.Agent) subagents.Runner {
24+ return subagents.RunnerFunc(func(ctx context.Context, sink subagents.Sink) error {
2625 <-ctx.Done()
2726 return ctx.Err()
2827 })
2928 },
3029 })
3130 iv := &Interactive{
32- swarmDialog: newSwarmDialog(),
33- dirty: make(chan struct{}, 1),
31+ subagentsDialog: newSubagentsDialog(),
32+ dirty: make(chan struct{}, 1),
3433 }
35- iv.cfg.Swarm = f
34+ iv.cfg.Supervisor = f
3635 return iv, f
3736 }
3837
39-// TestRunSwarmBareDoesNotPanic regression-tests the slice-out-of-range
40-// panic that hit when /swarm was typed with no subcommand: runSwarm
38+// TestRunSupervisorBareDoesNotPanic regression-tests the slice-out-of-range
39+// panic that hit when /subagents was typed with no subcommand: runSubagents
4140 // did args[1:] without checking len(args), which panics as [1:0].
42-func TestRunSwarmBareDoesNotPanic(t *testing.T) {
43- iv, _ := newInteractiveForSwarmTest(t)
44- defer iv.cfg.Swarm.StopAll()
41+func TestRunSupervisorBareDoesNotPanic(t *testing.T) {
42+ iv, _ := newInteractiveForSupervisorTest(t)
43+ defer iv.cfg.Supervisor.StopAll()
4544
46- // Bare /swarm: parts[1:] from the dispatcher is an empty slice.
47- iv.runSwarm(context.Background(), nil)
45+ // Bare /subagents: parts[1:] from the dispatcher is an empty slice.
46+ iv.runSubagents(context.Background(), nil)
4847
49- if !iv.swarmDialog.Active() {
50- t.Fatal("bare /swarm should open the dashboard")
48+ if !iv.subagentsDialog.Active() {
49+ t.Fatal("bare /subagents should open the dashboard")
5150 }
5251 }
5352
54-func TestRunSwarmSubcommandsDoNotPanic(t *testing.T) {
55- iv, _ := newInteractiveForSwarmTest(t)
56- defer iv.cfg.Swarm.StopAll()
53+func TestRunSupervisorSubcommandsDoNotPanic(t *testing.T) {
54+ iv, _ := newInteractiveForSupervisorTest(t)
55+ defer iv.cfg.Supervisor.StopAll()
5756
58- // Each row is the slice that the dispatcher hands to runSwarm —
57+ // Each row is the slice that the dispatcher hands to runSubagents —
5958 // i.e. parts[1:] where parts was strings.Fields of the slash
6059 // command. Mixing zero-arg and arg'd forms exercises both
6160 // branches of the reslice guard.
@@ -76,19 +75,19 @@ func TestRunSwarmSubcommandsDoNotPanic(t *testing.T) {
7675 func() {
7776 defer func() {
7877 if r := recover(); r != nil {
79- t.Fatalf("runSwarm(%v) panicked: %v", args, r)
78+ t.Fatalf("runSubagents(%v) panicked: %v", args, r)
8079 }
8180 }()
82- iv.runSwarm(context.Background(), args)
81+ iv.runSubagents(context.Background(), args)
8382 }()
8483 }
8584 }
8685
87-func TestRunSwarmRejectsNamedProfileWithoutResolver(t *testing.T) {
88- iv, f := newInteractiveForSwarmTest(t)
86+func TestRunSupervisorRejectsNamedProfileWithoutResolver(t *testing.T) {
87+ iv, f := newInteractiveForSupervisorTest(t)
8988 defer f.StopAll()
9089
91- iv.runSwarm(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})
90+ iv.runSubagents(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})
9291 if !strings.Contains(iv.statusErr, "named subagent profiles are unavailable") {
9392 t.Fatalf("status error = %q, want unavailable profile resolver", iv.statusErr)
9493 }
@@ -97,11 +96,11 @@ func TestRunSwarmRejectsNamedProfileWithoutResolver(t *testing.T) {
9796 }
9897 }
9998
100-func TestRunSwarmNewSpawnsAgent(t *testing.T) {
101- iv, f := newInteractiveForSwarmTest(t)
99+func TestRunSupervisorNewSpawnsAgent(t *testing.T) {
100+ iv, f := newInteractiveForSupervisorTest(t)
102101 defer f.StopAll()
103102
104- iv.runSwarm(context.Background(), []string{"new", "do", "stuff"})
103+ iv.runSubagents(context.Background(), []string{"new", "do", "stuff"})
105104 agents := f.List()
106105 if len(agents) != 1 {
107106 t.Fatalf("want 1 agent; got %d", len(agents))
@@ -111,23 +110,23 @@ func TestRunSwarmNewSpawnsAgent(t *testing.T) {
111110 }
112111 }
113112
114-func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) {
113+func TestRunSupervisorNamedProfileAppliesFastModeRestriction(t *testing.T) {
115114 root := t.TempDir()
116115 profileFastMode := false
117- f := swarm.New(swarm.Config{
116+ f := subagents.New(subagents.Config{
118117 Root: root,
119118 RepoRoot: root,
120119 FastMode: true,
121- NewRunner: func(*swarm.Agent) swarm.Runner {
122- return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error {
120+ NewRunner: func(*subagents.Agent) subagents.Runner {
121+ return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error {
123122 <-ctx.Done()
124123 return ctx.Err()
125124 })
126125 },
127126 })
128127 defer f.StopAll()
129- iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)}
130- iv.cfg.Swarm = f
128+ iv := &Interactive{subagentsDialog: newSubagentsDialog(), dirty: make(chan struct{}, 1)}
129+ iv.cfg.Supervisor = f
131130 iv.cfg.ResolveSubagent = func(name string) (*subagents.Profile, error) {
132131 if name != "reviewer" {
133132 return nil, nil
@@ -135,34 +134,34 @@ func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) {
135134 return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil
136135 }
137136
138- iv.runSwarm(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})
137+ iv.runSubagents(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})
139138 agents := f.List()
140139 if len(agents) != 1 || agents[0].FastMode {
141140 t.Fatalf("agent fast mode = %#v, want disabled by profile", agents)
142141 }
143142 }
144143
145-// TestRunSwarmSendDeliversToAgentInbox spins up a real agent with a
144+// TestRunSupervisorSendDeliversToAgentInbox spins up a real agent with a
146145 // fake Runner whose only job is to forward inbox lines to a channel,
147-// then asserts the /swarm send <id> <text...> path routes through
148-// Swarm.SendUserTurn and lands at the agent verbatim.
149-func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) {
146+// then asserts the /subagents send <id> <text...> path routes through
147+// Supervisor.SendUserTurn and lands at the agent verbatim.
148+func TestRunSupervisorSendDeliversToAgentInbox(t *testing.T) {
150149 if runtime.GOOS == "windows" {
151- t.Skip("swarm inbox transport uses Unix-domain sockets")
150+ t.Skip("subagent inbox transport uses Unix-domain sockets")
152151 }
153152 root := t.TempDir()
154153 recv := make(chan string, 4)
155154 ready := make(chan error, 1)
156- f := swarm.New(swarm.Config{
155+ f := subagents.New(subagents.Config{
157156 Root: root,
158157 RepoRoot: root,
159- NewRunner: func(a *swarm.Agent) swarm.Runner {
160- return swarm.RunnerFunc(func(ctx context.Context, sink swarm.Sink) error {
158+ NewRunner: func(a *subagents.Agent) subagents.Runner {
159+ return subagents.RunnerFunc(func(ctx context.Context, sink subagents.Sink) error {
161160 // Stand up a real Listener on the agent's inbox path so
162161 // SendUserTurn (which dials a unix socket) actually has
163162 // something to talk to. The runner-test stubs do the
164163 // same; this is the minimum to exercise the wire.
165- ln, err := swarm.Listen(a.InboxPath)
164+ ln, err := subagents.Listen(a.InboxPath)
166165 ready <- err
167166 if err != nil {
168167 return err
@@ -183,8 +182,8 @@ func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) {
183182 },
184183 })
185184 defer f.StopAll()
186- iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)}
187- iv.cfg.Swarm = f
185+ iv := &Interactive{subagentsDialog: newSubagentsDialog(), dirty: make(chan struct{}, 1)}
186+ iv.cfg.Supervisor = f
188187
189188 a, err := f.Spawn(context.Background(), "do thing")
190189 if err != nil {
@@ -199,15 +198,25 @@ func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) {
199198 t.Fatal("timed out waiting for agent inbox listener")
200199 }
201200
202- // Run /swarm send <id> <text...>. The dispatcher would have
201+ // Run /subagents send <id> <text...>. The dispatcher would have
203202 // already strings.Fields-ed the input; mirror that here.
204- iv.runSwarm(context.Background(), []string{"send", a.ID, "please", "continue"})
203+ iv.runSubagents(context.Background(), []string{"send", a.ID, "please", "continue"})
205204
206205 select {
207206 case msg := <-recv:
208- want := "user please continue"
209- if msg != want {
210- t.Fatalf("agent received %q; want %q", msg, want)
207+ command, parseErr := subagents.ParseCommand(msg)
208+ if parseErr != nil {
209+ t.Fatalf("agent received invalid command %q: %v", msg, parseErr)
210+ }
211+ if command.Type != subagents.CommandTurnStart {
212+ t.Fatalf("agent received command %q; want %q", command.Type, subagents.CommandTurnStart)
213+ }
214+ var payload subagents.TurnStartPayload
215+ if err := command.DecodePayload(&payload); err != nil {
216+ t.Fatalf("decode turn.start payload: %v", err)
217+ }
218+ if payload.Prompt != "please continue" {
219+ t.Fatalf("agent received prompt %q; want %q", payload.Prompt, "please continue")
211220 }
212221 case <-time.After(2 * time.Second):
213222 t.Fatal("timed out waiting for agent to receive the prompt")
@@ -298,18 +307,18 @@ func TestSplitIDAndRest(t *testing.T) {
298307 }
299308 }
300309
301-func TestRunSwarmWithoutSwarmIsNoop(t *testing.T) {
310+func TestRunSupervisorWithoutSupervisorIsNoop(t *testing.T) {
302311 iv := &Interactive{
303- swarmDialog: newSwarmDialog(),
304- dirty: make(chan struct{}, 1),
312+ subagentsDialog: newSubagentsDialog(),
313+ dirty: make(chan struct{}, 1),
305314 }
306- // cfg.Swarm stays nil. The command should set a status err and
315+ // cfg.Supervisor stays nil. The command should set a status err and
307316 // otherwise be inert.
308- iv.runSwarm(context.Background(), nil)
309- if iv.swarmDialog.Active() {
310- t.Fatal("dialog opened despite no swarm")
317+ iv.runSubagents(context.Background(), nil)
318+ if iv.subagentsDialog.Active() {
319+ t.Fatal("dialog opened despite no subagent")
311320 }
312321 if iv.statusErr == "" {
313- t.Fatal("expected a status error when swarm is nil")
322+ t.Fatal("expected a status error when subagent is nil")
314323 }
315324 }
M packages/agent/settings_store.go +10 -10
@@ -59,12 +59,12 @@ func (configSettingsStore) SetTerminalTitleEnabled(enabled bool) error {
5959 return SaveConfig(cfg)
6060 }
6161
62-func (configSettingsStore) SetAutoSwarm(enabled bool) error {
62+func (configSettingsStore) SetAutoSubagents(enabled bool) error {
6363 cfg, err := LoadConfig()
6464 if err != nil {
6565 return err
6666 }
67- cfg.AutoSwarmEnabled = &enabled
67+ cfg.AutoSubagentsEnabled = &enabled
6868 return SaveConfig(cfg)
6969 }
7070
@@ -212,24 +212,24 @@ func (configSettingsStore) SetTheme(name string) error {
212212 return SaveConfig(cfg)
213213 }
214214
215-// AutoSwarmEnabled reads the current auto-swarm flag from config.
216-// Used by the subagent_spawn/swarm_spawn tools at call time to gate execution.
217-func AutoSwarmEnabled() bool {
215+// AutoSubagentsEnabled reads the current auto-subagents flag from config.
216+// Used by the subagent_spawn tool at call time to gate execution.
217+func AutoSubagentsEnabled() bool {
218218 cfg, err := LoadConfig()
219219 if err != nil {
220220 return false
221221 }
222- return cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled
222+ return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled
223223 }
224224
225-// AutoSwarmSystemAddendum is appended to the system prompt when
226-// auto-swarm is enabled, so the model knows it may delegate to
225+// AutoSubagentsSystemAddendum is appended to the system prompt when
226+// auto-subagents is enabled, so the model knows it may delegate to
227227 // background sub-agents without the user having to mention the tool
228228 // by name. Kept short so it doesn't bloat the cached prompt prefix.
229-const AutoSwarmSystemAddendum = `Auto-swarm is enabled. You have a subagent_spawn tool (with swarm_spawn as a compatibility alias) that starts background sub-agents in separate long-lived processes.
229+const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You have a subagent_spawn tool that starts background sub-agents in separate long-lived processes.
230230
231231 Use it proactively when the user's request naturally splits into independent sub-tasks that can run concurrently; prefer isolation:"worktree" for parallel coding and use the returned result_ref/event path for completion (e.g. "refactor module A and module B", "write the implementation and the tests", "investigate three separate files"). Spawn one sub-agent per independent sub-task with a self-contained task description (sub-agents start with no context from this conversation). If [subagents_list] is present, choose the named profile whose description best matches the task and pass its name as the tool's agent field. Continue working on the remaining or coordinating work yourself in parallel; do not wait for sub-agents to finish before responding. Briefly tell the user which sub-agents you spawned and what each is doing.
232232
233233 Do NOT use subagent_spawn for trivial single-step work, for tasks that depend on each other sequentially, or when the user explicitly asked you to do the work yourself. Child workers cannot recursively spawn more sub-agents in v1.
234234
235-When every sub-agent you spawned reaches a terminal state, the host injects a single [auto-swarm update] message recapping each agent's status, task, and result reference. Treat that message as observed state (not as a new user request) and write a short follow-up summary referencing each agent by id.`
235+When every sub-agent you spawned reaches a terminal state, the host injects a single [auto-subagents update] message recapping each agent's status, task, and result reference. Treat that message as observed state (not as a new user request) and write a short follow-up summary referencing each agent by id.`
R packages/agent/swarm_agent.go → packages/agent/subagent_worker.go +56 -108 renamed, 75% similar
@@ -1,7 +1,6 @@
11 package agent
22
33 import (
4- "bytes"
54 "context"
65 "encoding/json"
76 "errors"
@@ -15,40 +14,40 @@ import (
1514 "unicode/utf8"
1615
1716 "github.com/patriceckhart/zot/packages/agent/modes"
18- "github.com/patriceckhart/zot/packages/agent/swarm"
17+ "github.com/patriceckhart/zot/packages/agent/subagents"
1918 "github.com/patriceckhart/zot/packages/core"
2019 "github.com/patriceckhart/zot/packages/provider"
2120 )
2221
23-// runSwarmAgentMode is the daemon-mode entry point used by every
24-// swarm-spawned zot subprocess. It's intentionally close in shape to
22+// runSubagentWorkerMode is the daemon-mode entry point used by every
23+// subagent-spawned zot subprocess. It's intentionally close in shape to
2524 // runJSONMode but with two key differences:
2625 //
2726 // - Lifetime: the process stays alive across many user turns. The
2827 // initial positional task (if any) is the first turn; subsequent
29-// turns arrive through the inbox unix socket at args.SwarmAgent.
28+// turns arrive through the inbox unix socket at args.SubagentWorker.
3029 //
3130 // - Output: every emitted JSON line is also mirrored verbatim into
32-// events.jsonl (see ZOT_SWARM_EVENT_LOG) so a separate zot
33-// process can /swarm open this agent and replay its full history
31+// events.jsonl (see ZOT_SUBAGENT_EVENT_LOG) so a separate zot
32+// process can /subagents open this agent and replay its full history
3433 // even after the parent that spawned us is long gone.
3534 //
36-// The runner in packages/agent/swarm/runner.go is the only caller in
35+// The runner in packages/agent/subagents/runner.go is the only caller in
3736 // production; tests use the stubchild binary under
38-// packages/agent/swarm/testdata/cmd/stubchild instead of the real model
37+// packages/agent/subagents/testdata/cmd/stubchild instead of the real model
3938 // loop.
40-func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
41- if args.SwarmAgent == "" {
42- return fmt.Errorf("--swarm-agent requires a socket path")
39+func runSubagentWorkerMode(ctx context.Context, args Args, version string) error {
40+ if args.SubagentWorker == "" {
41+ return fmt.Errorf("--subagent-worker requires a socket path")
4342 }
44- if os.Getenv("ZOT_SWARM_CREDENTIAL_STDIN") == "1" {
45- var inherited swarm.Credential
43+ if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_STDIN") == "1" {
44+ var inherited subagents.Credential
4645 dec := json.NewDecoder(io.LimitReader(os.Stdin, 1<<20))
4746 if err := dec.Decode(&inherited); err != nil {
48- return fmt.Errorf("read inherited swarm credential: %w", err)
47+ return fmt.Errorf("read inherited subagent credential: %w", err)
4948 }
5049 if inherited.Value == "" || inherited.Method == "" {
51- return fmt.Errorf("read inherited swarm credential: missing value or method")
50+ return fmt.Errorf("read inherited subagent credential: missing value or method")
5251 }
5352 args.inheritedCredential = inherited.Value
5453 args.inheritedAuthMethod = inherited.Method
@@ -89,12 +88,12 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
8988
9089 // Open the inbox listener BEFORE emitting agent_ready so the
9190 // supervisor can dial through on the very first send. The
92- // swarm supervisor's Inbox.SendInput retries dialing for a
91+ // subagent supervisor's Inbox retries dialing for a
9392 // short window, but emitting ready first and then listening
9493 // would still race in tight loops.
95- ln, err := swarm.Listen(args.SwarmAgent)
94+ ln, err := subagents.Listen(args.SubagentWorker)
9695 if err != nil {
97- return fmt.Errorf("swarm-agent listen: %w", err)
96+ return fmt.Errorf("subagent-worker listen: %w", err)
9897 }
9998 defer ln.Close()
10099
@@ -103,16 +102,16 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
103102 // pipe is closed (e.g. parent zot exited but the agent is still
104103 // running headless). The env var is set by the runner; if it's
105104 // empty we silently skip the second mirror.
106- var logMirror *swarm.EventLog
107- if path := os.Getenv("ZOT_SWARM_EVENT_LOG"); path != "" {
108- logMirror, _ = swarm.OpenEventLog(path)
105+ var logMirror *subagents.EventLog
106+ if path := os.Getenv("ZOT_SUBAGENT_EVENT_LOG"); path != "" {
107+ logMirror, _ = subagents.OpenEventLog(path)
109108 }
110109 if logMirror != nil {
111110 defer logMirror.Close()
112111 }
113112
114- em := newSwarmEmitter(os.Stdout, logMirror)
115- em.setProtocolIdentity(os.Getenv("ZOT_SWARM_AGENT_ID"))
113+ em := newSubagentEmitter(os.Stdout, logMirror)
114+ em.setProtocolIdentity(os.Getenv("ZOT_SUBAGENT_AGENT_ID"))
116115 em.emit("agent.ready", map[string]any{
117116 "version": version,
118117 "cwd": r.CWD,
@@ -126,7 +125,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
126125 defer close(heartbeatDone)
127126 go func() {
128127 interval := 10 * time.Second
129- if raw := os.Getenv("ZOT_SWARM_HEARTBEAT_INTERVAL"); raw != "" {
128+ if raw := os.Getenv("ZOT_SUBAGENT_HEARTBEAT_INTERVAL"); raw != "" {
130129 if parsed, parseErr := time.ParseDuration(raw); parseErr == nil && parsed > 0 {
131130 interval = parsed
132131 }
@@ -149,7 +148,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
149148 mu sync.Mutex
150149 cancelFn context.CancelFunc
151150 busyTurn bool
152- turnNo = initialTurnNumber(os.Getenv("ZOT_SWARM_EVENT_LOG"))
151+ turnNo = initialTurnNumber(os.Getenv("ZOT_SUBAGENT_EVENT_LOG"))
153152 turnWG sync.WaitGroup
154153 closing bool
155154 shutdown = make(chan struct{})
@@ -279,7 +278,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
279278
280279 // Initial task: run before processing the inbox so the agent
281280 // "starts working" the moment it boots, matching what users
282- // expect from `/swarm new <task>`.
281+ // expect from `/subagents new <task>`.
283282 if args.Prompt != "" {
284283 launchTurn(args.Prompt)
285284 }
@@ -308,24 +307,24 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
308307 em.emit("agent.exited", map[string]any{"reason": "inbox-closed"})
309308 return nil
310309 }
311- command, parseErr := swarm.ParseCommand(msg)
310+ command, parseErr := subagents.ParseCommand(msg)
312311 if parseErr != nil {
313312 em.emit("error", map[string]any{"message": "invalid supervisor command"})
314313 continue
315314 }
316315 switch command.Type {
317- case swarm.CommandAgentShutdown:
316+ case subagents.CommandAgentShutdown:
318317 requestShutdown()
319- case swarm.CommandTurnCancel:
318+ case subagents.CommandTurnCancel:
320319 mu.Lock()
321320 if cancelFn != nil {
322321 cancelFn()
323322 }
324323 mu.Unlock()
325- case swarm.CommandAgentPing:
324+ case subagents.CommandAgentPing:
326325 em.emit("agent.heartbeat", map[string]any{"activity": "alive"})
327- case swarm.CommandTurnStart:
328- var payload swarm.TurnStartPayload
326+ case subagents.CommandTurnStart:
327+ var payload subagents.TurnStartPayload
329328 if err := command.DecodePayload(&payload); err != nil {
330329 em.emit("error", map[string]any{"message": "invalid turn.start payload"})
331330 continue
@@ -340,15 +339,14 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
340339 }
341340 }
342341
343-// swarmEmitter serialises events to stdout and (optionally) to a
342+// subagentEmitter serialises events to stdout and (optionally) to a
344343 // durable log file. Concurrent goroutines call emit so we have to
345344 // hold a mutex around the encoder.
346-type swarmEmitter struct {
347- mu sync.Mutex
348- w *os.File
349- mirror *swarm.EventLog
350- agentID string
351- versioned bool
345+type subagentEmitter struct {
346+ mu sync.Mutex
347+ w *os.File
348+ mirror *subagents.EventLog
349+ agentID string
352350
353351 // orphan flips true the first time a stdout write fails (broken
354352 // pipe — the supervisor died). Until then the mirror stays
@@ -362,18 +360,17 @@ type swarmEmitter struct {
362360 orphan bool
363361 }
364362
365-func newSwarmEmitter(w *os.File, mirror *swarm.EventLog) *swarmEmitter {
366- return &swarmEmitter{w: w, mirror: mirror}
363+func newSubagentEmitter(w *os.File, mirror *subagents.EventLog) *subagentEmitter {
364+ return &subagentEmitter{w: w, mirror: mirror}
367365 }
368366
369-func (e *swarmEmitter) setProtocolIdentity(agentID string) {
367+func (e *subagentEmitter) setProtocolIdentity(agentID string) {
370368 e.mu.Lock()
371369 e.agentID = agentID
372- e.versioned = true
373370 e.mu.Unlock()
374371 }
375372
376-func (e *swarmEmitter) emit(typ string, data map[string]any) {
373+func (e *subagentEmitter) emit(typ string, data map[string]any) {
377374 if data == nil {
378375 data = map[string]any{}
379376 }
@@ -386,45 +383,9 @@ func (e *swarmEmitter) emit(typ string, data map[string]any) {
386383 e.mu.Lock()
387384 defer e.mu.Unlock()
388385
389- wireType := typ
390- if e.versioned {
391- wireType = canonicalWorkerEvent(typ)
392- }
393- var line []byte
394- var err error
395- if e.versioned {
396- envelope := swarm.NewEventEnvelope(wireType, e.agentID, turnID, payload)
397- line, err = swarm.MarshalJSONL(envelope)
398- if err == nil {
399- // Flatten payload fields as a compatibility courtesy. New peers
400- // read `payload`; old peers can still inspect `step`, `content`,
401- // and similar fields at the top level.
402- var object map[string]any
403- if json.Unmarshal(bytes.TrimSpace(line), &object) == nil {
404- for key, value := range payload {
405- if _, exists := object[key]; exists {
406- continue
407- }
408- // Keep the compatibility flattening useful for old peers,
409- // but do not duplicate large result/output fields on the
410- // wire. New peers read the canonical payload object; a
411- // duplicated 500 KiB output would otherwise exceed the
412- // supervisor's bounded JSONL frame budget.
413- if !compatibilityFlatValue(value) {
414- continue
415- }
416- object[key] = value
417- }
418- line, err = json.Marshal(object)
419- line = append(line, '\n')
420- }
421- }
422- } else {
423- payload["type"] = typ
424- payload["time"] = time.Now().Format(time.RFC3339Nano)
425- line, err = json.Marshal(payload)
426- line = append(line, '\n')
427- }
386+ wireType := canonicalWorkerEvent(typ)
387+ envelope := subagents.NewEventEnvelope(wireType, e.agentID, turnID, payload)
388+ line, err := subagents.MarshalJSONL(envelope)
428389
429390 if !e.orphan && err == nil {
430391 if _, werr := e.w.Write(line); werr != nil {
@@ -436,39 +397,26 @@ func (e *swarmEmitter) emit(typ string, data map[string]any) {
436397 }
437398
438399 if e.orphan && e.mirror != nil {
439- _ = e.mirror.Append(swarm.NewEvent(wireType, payload))
440- }
441-}
442-
443-const compatibilityFlatValueBytes = 64 * 1024
444-
445-func compatibilityFlatValue(value any) bool {
446- switch v := value.(type) {
447- case string:
448- return len(v) <= compatibilityFlatValueBytes
449- case json.RawMessage:
450- return len(v) <= compatibilityFlatValueBytes
451- default:
452- return true
400+ _ = e.mirror.Append(subagents.NewEvent(wireType, payload))
453401 }
454402 }
455403
456404 func canonicalWorkerEvent(typ string) string {
457405 switch typ {
458406 case "agent_ready":
459- return swarm.EventAgentReady
407+ return subagents.EventAgentReady
460408 case "agent_stopped":
461- return swarm.EventAgentExited
409+ return subagents.EventAgentExited
462410 case "turn_start":
463- return swarm.EventTurnStarted
411+ return subagents.EventTurnStarted
464412 case "turn_progress":
465- return swarm.EventTurnProgress
413+ return subagents.EventTurnProgress
466414 case "tool_call":
467- return swarm.EventToolStarted
415+ return subagents.EventToolStarted
468416 case "tool_result":
469- return swarm.EventToolFinished
417+ return subagents.EventToolFinished
470418 case "text_delta":
471- return swarm.EventMessageDelta
419+ return subagents.EventMessageDelta
472420 default:
473421 return typ
474422 }
@@ -502,10 +450,10 @@ func boundedResultSummary(text string) string {
502450
503451 func workerOutputLimits() (maxBytes, maxLines int) {
504452 maxBytes, maxLines = 500_000, 5_000
505- if value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("ZOT_SWARM_MAX_OUTPUT_BYTES"))); err == nil && value > 0 {
453+ if value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES"))); err == nil && value > 0 {
506454 maxBytes = value
507455 }
508- if value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("ZOT_SWARM_MAX_OUTPUT_LINES"))); err == nil && value > 0 {
456+ if value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES"))); err == nil && value > 0 {
509457 maxLines = value
510458 }
511459 return maxBytes, maxLines
@@ -563,7 +511,7 @@ func initialTurnNumber(path string) int {
563511 if strings.TrimSpace(path) == "" {
564512 return 0
565513 }
566- events, err := swarm.ReadEventLog(path)
514+ events, err := subagents.ReadEventLog(path)
567515 if err != nil {
568516 return 0
569517 }
R packages/agent/swarm_agent_test.go → packages/agent/subagent_worker_test.go +27 -26 renamed, 77% similar
@@ -9,29 +9,29 @@ import (
99 "strings"
1010 "testing"
1111
12- "github.com/patriceckhart/zot/packages/agent/swarm"
12+ "github.com/patriceckhart/zot/packages/agent/subagents"
1313 )
1414
1515 // TestWorkerOutputLimitsUseSupervisorPolicy verifies that the child reads
1616 // the effective output caps propagated by the supervisor and retains safe
1717 // defaults for standalone workers.
1818 func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) {
19- t.Setenv("ZOT_SWARM_MAX_OUTPUT_BYTES", "123")
20- t.Setenv("ZOT_SWARM_MAX_OUTPUT_LINES", "7")
19+ t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES", "123")
20+ t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES", "7")
2121 bytesLimit, linesLimit := workerOutputLimits()
2222 if bytesLimit != 123 || linesLimit != 7 {
2323 t.Fatalf("worker output limits = (%d, %d), want (123, 7)", bytesLimit, linesLimit)
2424 }
25- t.Setenv("ZOT_SWARM_MAX_OUTPUT_BYTES", "invalid")
26- t.Setenv("ZOT_SWARM_MAX_OUTPUT_LINES", "0")
25+ t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES", "invalid")
26+ t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES", "0")
2727 bytesLimit, linesLimit = workerOutputLimits()
2828 if bytesLimit != 500_000 || linesLimit != 5_000 {
2929 t.Fatalf("invalid worker output limits = (%d, %d), want defaults", bytesLimit, linesLimit)
3030 }
3131 }
3232
33-// TestSwarmEmitterMirrorDormantUntilStdoutBreaks regresses the
34-// "everything is doubled after reopening a swarm agent" bug.
33+// TestSupervisorEmitterMirrorDormantUntilStdoutBreaks regresses the
34+// "everything is doubled after reopening a subagent agent" bug.
3535 //
3636 // Symptom: events.jsonl held two copies of every event because the
3737 // child mirrored each event to disk AND the supervisor parsed the
@@ -42,7 +42,7 @@ func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) {
4242 // alive on the other end of the pipe), the child's mirror writes
4343 // NOTHING. Only when a stdout write fails (broken pipe → orphan)
4444 // does the mirror take over so events still get persisted.
45-func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
45+func TestSupervisorEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
4646 // Real *os.File for the emitter's stdout-equivalent so the
4747 // emitter's write() path (which expects *os.File) actually runs.
4848 stdoutPath := filepath.Join(t.TempDir(), "stdout.fifo")
@@ -55,20 +55,20 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
5555 // Mirror writes go to a separate events.jsonl that we can read
5656 // at the end to assert how many events the mirror emitted.
5757 mirrorPath := filepath.Join(t.TempDir(), "events.jsonl")
58- mirror, err := swarm.OpenEventLog(mirrorPath)
58+ mirror, err := subagents.OpenEventLog(mirrorPath)
5959 if err != nil {
6060 t.Fatalf("open mirror: %v", err)
6161 }
6262 defer mirror.Close()
6363
64- em := newSwarmEmitter(stdoutFile, mirror)
64+ em := newSubagentEmitter(stdoutFile, mirror)
6565
6666 // Healthy stdout: emit three events. Mirror must stay empty.
6767 em.emit("turn_start", map[string]any{"step": 1})
6868 em.emit("assistant_message", map[string]any{"text": "hi"})
6969 em.emit("turn_end", map[string]any{"step": 1})
7070
71- got, err := swarm.ReadEventLog(mirrorPath)
71+ got, err := subagents.ReadEventLog(mirrorPath)
7272 if err != nil {
7373 t.Fatalf("read mirror: %v", err)
7474 }
@@ -87,7 +87,7 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
8787 em.emit("assistant_message", map[string]any{"text": "after orphan"})
8888 em.emit("turn_end", map[string]any{"step": 2})
8989
90- got, err = swarm.ReadEventLog(mirrorPath)
90+ got, err = subagents.ReadEventLog(mirrorPath)
9191 if err != nil {
9292 t.Fatalf("read mirror post-orphan: %v", err)
9393 }
@@ -99,11 +99,11 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
9999 }
100100 }
101101
102-// TestSwarmEmitterStdoutShapeMatchesSupervisorParser pins the
102+// TestSupervisorEmitterStdoutShapeMatchesSupervisorParser pins the
103103 // wire-format contract: each emitted event lands on stdout as one
104104 // JSON object per line with type+time at top level alongside the
105105 // data fields. The supervisor's runner parses this exact shape.
106-func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
106+func TestSupervisorEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
107107 path := filepath.Join(t.TempDir(), "events.jsonl")
108108 file, err := os.Create(path)
109109 if err != nil {
@@ -111,7 +111,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
111111 }
112112 defer file.Close()
113113
114- em := newSwarmEmitter(file, nil)
114+ em := newSubagentEmitter(file, nil)
115115 em.setProtocolIdentity("agent-1")
116116 em.emit("turn.result", map[string]any{
117117 "status": "succeeded",
@@ -139,7 +139,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
139139 if !ok || len(payload["output"].(string)) != 500_000 {
140140 t.Fatal("canonical payload lost the complete bounded output")
141141 }
142- events, err := swarm.ReadEventLog(path)
142+ events, err := subagents.ReadEventLog(path)
143143 if err != nil || len(events) != 1 {
144144 t.Fatalf("supervisor event parser could not recover the large result: events=%d err=%v", len(events), err)
145145 }
@@ -148,7 +148,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
148148 }
149149 }
150150
151-func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) {
151+func TestSupervisorEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) {
152152 // Pipe so we can read what the emitter wrote.
153153 r, w, err := os.Pipe()
154154 if err != nil {
@@ -156,7 +156,7 @@ func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) {
156156 }
157157 defer r.Close()
158158
159- em := newSwarmEmitter(w, nil)
159+ em := newSubagentEmitter(w, nil)
160160 em.emit("turn_start", map[string]any{"step": 1})
161161 _ = w.Close()
162162
@@ -169,17 +169,18 @@ func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) {
169169 if len(lines) != 1 {
170170 t.Fatalf("expected 1 event line, got %d: %q", len(lines), body)
171171 }
172- var flat map[string]any
173- if err := json.Unmarshal(lines[0], &flat); err != nil {
172+ var object map[string]any
173+ if err := json.Unmarshal(lines[0], &object); err != nil {
174174 t.Fatalf("not valid json: %v\n%s", err, lines[0])
175175 }
176- if flat["type"] != "turn_start" {
177- t.Errorf("type field missing or wrong: %v", flat["type"])
176+ if object["type"] != "turn.started" {
177+ t.Errorf("type field missing or wrong: %v", object["type"])
178178 }
179- if _, ok := flat["time"].(string); !ok {
180- t.Errorf("time field missing: %v", flat["time"])
179+ if _, ok := object["timestamp"].(string); !ok {
180+ t.Errorf("timestamp field missing: %v", object["timestamp"])
181181 }
182- if flat["step"] != float64(1) {
183- t.Errorf("step field missing or wrong: %v", flat["step"])
182+ payload, ok := object["payload"].(map[string]any)
183+ if !ok || payload["step"] != float64(1) {
184+ t.Errorf("payload step field missing or wrong: %v", object["payload"])
184185 }
185186 }
R packages/agent/swarm/agent.go → packages/agent/subagents/agent.go +10 -28 renamed, 93% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -59,10 +59,9 @@ type Agent struct {
5959
6060 // SessionID, when non-empty, scopes the agent to a particular
6161 // host zot session: the dashboard only surfaces agents whose
62- // SessionID matches the active session. Empty means "unscoped"
63- // (legacy meta files from before the field existed, or agents
64- // spawned without a session context such as in tests). Set at
65- // Spawn time from Swarm.activeSession and persisted in
62+ // SessionID matches the active session. Empty means "unscoped" and
63+ // applies to agents spawned without a session context such as in tests. Set at
64+ // Spawn time from Supervisor.activeSession and persisted in
6665 // meta.json so the scope survives restarts.
6766 SessionID string
6867
@@ -93,17 +92,16 @@ type Agent struct {
9392
9493 // EventLogPath is the durable JSONL event log for this agent.
9594 // The runner appends every well-formed event from the child
96- // (plus lifecycle events of its own) here. /swarm open in any
95+ // (plus lifecycle events of its own) here. /subagents open in any
9796 // zot process reads from this file to replay the full history.
9897 EventLogPath string
9998
10099 // SessionPath is the child's persistent session file. Surfaced
101- // so the dashboard / /swarm open can resume the agent later.
100+ // so the dashboard / /subagents open can resume the agent later.
102101 SessionPath string
103102
104103 // inbox is the supervisor-side socket handle. Populated by
105- // Swarm.Spawn so SendInput can route messages without each
106- // caller redialing.
104+ // Supervisor.Spawn so callers do not need to manage the socket directly.
107105 inbox *Inbox
108106
109107 mu sync.Mutex
@@ -117,7 +115,6 @@ type Agent struct {
117115 lifecycleMu sync.Mutex
118116 processState ProcessState
119117 turnState TurnState
120- protocolVersion int
121118 currentTurnID string
122119 updatedAt time.Time
123120 lastActivity time.Time
@@ -136,10 +133,10 @@ type Agent struct {
136133 workspaceCapture func() (WorkspaceCapture, error)
137134
138135 // OnTurnEnd, if set, fires once per prompt-level turn_end event
139- // emitted by the swarm daemon wrapper. Provider/tool-loop
136+ // emitted by the subagent daemon wrapper. Provider/tool-loop
140137 // turn_end events (for example stop=tool_use) are ignored by the
141138 // runner because they do not mean the sub-agent task finished.
142- // Used by auto-swarm watchers to detect that a sub-agent's first
139+ // Used by auto-subagents watchers to detect that a sub-agent's first
143140 // (or n-th) task has finished without waiting for the long-lived
144141 // daemon itself to exit — sub-agents keep running on the inbox
145142 // even after the initial task completes, so Wait() never unblocks
@@ -226,21 +223,6 @@ func (a *Agent) Result() *TurnResult {
226223 return cloneTurnResult(a.result)
227224 }
228225
229-func (a *Agent) setProtocolVersion(version int) {
230- a.lifecycleMu.Lock()
231- if version > a.protocolVersion {
232- a.protocolVersion = version
233- }
234- a.updatedAt = time.Now()
235- a.lifecycleMu.Unlock()
236-}
237-
238-func (a *Agent) protocolReady() bool {
239- a.lifecycleMu.Lock()
240- defer a.lifecycleMu.Unlock()
241- return a.protocolVersion >= ProtocolVersion
242-}
243-
244226 func (a *Agent) setProcessState(state ProcessState) {
245227 a.lifecycleMu.Lock()
246228 a.processState = state
@@ -319,7 +301,7 @@ func (a *Agent) Err() error {
319301 }
320302
321303 // Wait blocks until the agent reaches a terminal state. Used by tests
322-// and by /swarm wait <id>.
304+// and by /subagents wait <id>.
323305 func (a *Agent) Wait() { <-a.done }
324306
325307 func (a *Agent) waitForTurnResult(ctx context.Context) (*TurnResult, error) {
R packages/agent/swarm/batch.go → packages/agent/subagents/batch.go +8 -8 renamed, 96% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -76,7 +76,7 @@ func (b *Batch) Result() *BatchResult {
7676 func (b *Batch) Wait() { <-b.done }
7777 func (b *Batch) closeDone() { b.once.Do(func() { close(b.done) }) }
7878
79-func (f *Swarm) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error) {
79+func (f *Supervisor) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error) {
8080 if len(req.Tasks) == 0 {
8181 return nil, fmt.Errorf("subagents: batch requires at least one task")
8282 }
@@ -166,7 +166,7 @@ func (f *Swarm) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error
166166 return batch, nil
167167 }
168168
169-func (f *Swarm) collectBatch(batch *Batch) {
169+func (f *Supervisor) collectBatch(batch *Batch) {
170170 if batch.TaskCount > 0 && len(batch.ChildIDs) < batch.TaskCount {
171171 batch.mu.Lock()
172172 batch.status = BatchFailed
@@ -228,7 +228,7 @@ func (f *Swarm) collectBatch(batch *Batch) {
228228 batch.closeDone()
229229 }
230230
231-func (f *Swarm) waitForBatchResult(a *Agent) (*TurnResult, error) {
231+func (f *Supervisor) waitForBatchResult(a *Agent) (*TurnResult, error) {
232232 if a == nil {
233233 return nil, errors.New("subagents: missing batch child")
234234 }
@@ -293,7 +293,7 @@ func (f *Swarm) waitForBatchResult(a *Agent) (*TurnResult, error) {
293293 }
294294 }
295295
296-func (f *Swarm) WaitBatch(id string) (*BatchResult, error) {
296+func (f *Supervisor) WaitBatch(id string) (*BatchResult, error) {
297297 f.mu.Lock()
298298 batch := f.batches[id]
299299 f.mu.Unlock()
@@ -304,7 +304,7 @@ func (f *Swarm) WaitBatch(id string) (*BatchResult, error) {
304304 return batch.Result(), nil
305305 }
306306
307-func (f *Swarm) GetBatch(id string) *Batch {
307+func (f *Supervisor) GetBatch(id string) *Batch {
308308 f.mu.Lock()
309309 defer f.mu.Unlock()
310310 return f.batches[id]
@@ -325,7 +325,7 @@ func cloneBatchResult(result *BatchResult) *BatchResult {
325325 return &copy
326326 }
327327
328-func (f *Swarm) reloadBatches(root string) []error {
328+func (f *Supervisor) reloadBatches(root string) []error {
329329 entries, err := os.ReadDir(filepath.Join(root, "batches"))
330330 if err != nil {
331331 if errors.Is(err, os.ErrNotExist) {
@@ -402,7 +402,7 @@ func (f *Swarm) reloadBatches(root string) []error {
402402 return errs
403403 }
404404
405-func (f *Swarm) persistBatch(batch *Batch) {
405+func (f *Supervisor) persistBatch(batch *Batch) {
406406 if batch == nil || !safeAgentID(batch.ID) {
407407 return
408408 }
R packages/agent/swarm/batch_test.go → packages/agent/subagents/batch_test.go +4 -4 renamed, 96% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -36,7 +36,7 @@ func TestBatchIDsRemainUniqueWithFixedClock(t *testing.T) {
3636
3737 func TestBatchReloadsDurableResult(t *testing.T) {
3838 root := t.TempDir()
39- newSwarm := func() *Swarm {
39+ newSupervisor := func() *Supervisor {
4040 return New(Config{
4141 Root: root, RepoRoot: root,
4242 NewRunner: func(*Agent) Runner {
@@ -44,7 +44,7 @@ func TestBatchReloadsDurableResult(t *testing.T) {
4444 },
4545 })
4646 }
47- first := newSwarm()
47+ first := newSupervisor()
4848 t.Cleanup(first.StopAll)
4949 batch, err := first.SpawnBatch(context.Background(), BatchRequest{Context: "review", Tasks: []string{"one", "two"}})
5050 if err != nil {
@@ -58,7 +58,7 @@ func TestBatchReloadsDurableResult(t *testing.T) {
5858 t.Fatalf("first batch status = %s", want.Status)
5959 }
6060
61- second := newSwarm()
61+ second := newSupervisor()
6262 t.Cleanup(second.StopAll)
6363 loaded, errs := second.Reload()
6464 if len(errs) != 0 {
R packages/agent/swarm/event.go → packages/agent/subagents/event.go +53 -64 renamed, 81% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "bufio"
@@ -15,9 +15,9 @@ import (
1515 // Event is one structured datum in an agent's durable event log.
1616 //
1717 // The shape is intentionally a thin map+timestamp wrapper rather
18-// than a strict struct so the swarm package doesn't have to track
18+// than a strict struct so the subagent package doesn't have to track
1919 // every field added to core.AgentEvent. The child writes
20-// `modes.EventToJSON(ev)` plus a few swarm-internal types
20+// `modes.EventToJSON(ev)` plus a few subagent-internal types
2121 // (lifecycle, user_input echo, error). The supervisor doesn't
2222 // interpret most fields; it just keeps them for replay and lifts
2323 // a few well-known ones into AgentSnapshot (Activity, Status,
@@ -35,20 +35,29 @@ type Event struct {
3535 AgentID string `json:"agent_id,omitempty"`
3636 TurnID string `json:"turn_id,omitempty"`
3737 Data map[string]any `json:"-"`
38- Raw map[string]any `json:"-"` // includes type+time+data for replay
38+ Raw map[string]any `json:"-"` // original canonical event for replay
3939 }
4040
41-// MarshalJSON flattens Data into the top-level object so consumers
42-// see {type, time, ...fields} rather than {type, time, data:{...}}.
41+// MarshalJSON writes the canonical versioned event shape used by the
42+// worker protocol and durable event logs.
4343 func (e Event) MarshalJSON() ([]byte, error) {
44- out := make(map[string]any, len(e.Data)+7)
45- for k, v := range e.Data {
46- out[k] = v
44+ version := e.Version
45+ if version == 0 {
46+ version = ProtocolVersion
4747 }
48- out["type"] = e.Type
49- out["time"] = e.Time
50- if e.Version != 0 {
51- out["version"] = e.Version
48+ when := e.Time
49+ if when.IsZero() {
50+ when = time.Now().UTC()
51+ }
52+ payload := e.Data
53+ if payload == nil {
54+ payload = map[string]any{}
55+ }
56+ out := map[string]any{
57+ "version": version,
58+ "type": e.Type,
59+ "timestamp": when,
60+ "payload": payload,
5261 }
5362 if e.MessageID != "" {
5463 out["message_id"] = e.MessageID
@@ -59,67 +68,47 @@ func (e Event) MarshalJSON() ([]byte, error) {
5968 if e.TurnID != "" {
6069 out["turn_id"] = e.TurnID
6170 }
62- if e.Version != 0 {
63- out["timestamp"] = e.Time
64- out["payload"] = e.Data
65- }
6671 return json.Marshal(out)
6772 }
6873
69-// UnmarshalJSON accepts a flat object with at least type+time.
70-// All other fields land in both Data and Raw.
74+// UnmarshalJSON accepts the canonical versioned event shape and retains the
75+// decoded payload for dashboard replay.
7176 func (e *Event) UnmarshalJSON(b []byte) error {
72- m := map[string]any{}
73- if err := json.Unmarshal(b, &m); err != nil {
77+ var wire struct {
78+ Version int `json:"version"`
79+ Type string `json:"type"`
80+ MessageID string `json:"message_id,omitempty"`
81+ AgentID string `json:"agent_id,omitempty"`
82+ TurnID string `json:"turn_id,omitempty"`
83+ Timestamp time.Time `json:"timestamp"`
84+ Payload map[string]any `json:"payload"`
85+ }
86+ if err := json.Unmarshal(b, &wire); err != nil {
7487 return err
7588 }
76- e.Raw = m
77- e.Data = map[string]any{}
78- for k, v := range m {
79- if k == "type" || k == "time" || k == "timestamp" || k == "version" || k == "message_id" || k == "agent_id" || k == "turn_id" || k == "payload" {
80- continue
81- }
82- e.Data[k] = v
83- }
84- // Versioned events keep their forward-compatible payload under a
85- // dedicated object. The child also flattens known fields for legacy
86- // readers, but a newer event may contain payload fields that were not
87- // flattened by the producer. Merge those fields so replay sees the same
88- // data regardless of which wire shape was written.
89- if payload, ok := m["payload"].(map[string]any); ok {
90- for k, v := range payload {
91- if _, exists := e.Data[k]; !exists {
92- e.Data[k] = v
93- }
94- }
95- }
96- if version, ok := m["version"].(float64); ok {
97- e.Version = int(version)
98- }
99- if messageID, ok := m["message_id"].(string); ok {
100- e.MessageID = messageID
89+ if wire.Version != ProtocolVersion {
90+ return fmt.Errorf("subagent event: unsupported version %d", wire.Version)
10191 }
102- if agentID, ok := m["agent_id"].(string); ok {
103- e.AgentID = agentID
92+ if wire.Type == "" {
93+ return errors.New("subagent event: missing type")
10494 }
105- if turnID, ok := m["turn_id"].(string); ok {
106- e.TurnID = turnID
95+ if wire.Timestamp.IsZero() {
96+ return errors.New("subagent event: missing timestamp")
10797 }
108- if t, ok := m["type"].(string); ok {
109- e.Type = t
110- } else {
111- return errors.New("swarm event: missing type")
98+ if wire.Payload == nil {
99+ wire.Payload = map[string]any{}
112100 }
113- ts, _ := m["time"].(string)
114- if ts == "" {
115- ts, _ = m["timestamp"].(string)
116- }
117- if ts != "" {
118- parsed, err := time.Parse(time.RFC3339Nano, ts)
119- if err == nil {
120- e.Time = parsed
121- }
101+ e.Raw = map[string]any{}
102+ if err := json.Unmarshal(b, &e.Raw); err != nil {
103+ return err
122104 }
105+ e.Time = wire.Timestamp
106+ e.Type = wire.Type
107+ e.Version = wire.Version
108+ e.MessageID = wire.MessageID
109+ e.AgentID = wire.AgentID
110+ e.TurnID = wire.TurnID
111+ e.Data = wire.Payload
123112 return nil
124113 }
125114
@@ -202,7 +191,7 @@ func NewEvent(typ string, data map[string]any) Event {
202191 if data == nil {
203192 data = map[string]any{}
204193 }
205- return Event{Time: time.Now(), Type: typ, Data: data}
194+ return Event{Time: time.Now().UTC(), Type: typ, Version: ProtocolVersion, Data: data}
206195 }
207196
208197 // ReadEventLog parses every event currently in the file. Used by
R packages/agent/swarm/event_test.go → packages/agent/subagents/event_test.go +2 -2 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "bytes"
@@ -13,7 +13,7 @@ import (
1313
1414 // TestEventLogAppendAndRead is the simplest possible invariant:
1515 // what we write goes back through ReadEventLog exactly, including
16-// the flat field shape and the timestamp.
16+// the payload fields and the timestamp.
1717 func TestEventLogAppendAndRead(t *testing.T) {
1818 path := filepath.Join(t.TempDir(), "events.jsonl")
1919 log, err := OpenEventLog(path)
R packages/agent/swarm/fast_mode_test.go → packages/agent/subagents/fast_mode_test.go +10 -10 renamed, 88% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -6,7 +6,7 @@ import (
66 "testing"
77 )
88
9-func TestSwarmFastModePropagatesToChild(t *testing.T) {
9+func TestSupervisorFastModePropagatesToChild(t *testing.T) {
1010 root := t.TempDir()
1111 f := New(Config{
1212 Root: root,
@@ -103,9 +103,9 @@ func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) {
103103 }
104104 }
105105
106-func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
106+func TestSupervisorReloadPreservesExplicitFastModeOff(t *testing.T) {
107107 root := t.TempDir()
108- newSwarm := func(fastMode bool) *Swarm {
108+ newSupervisor := func(fastMode bool) *Supervisor {
109109 return New(Config{
110110 Root: root,
111111 RepoRoot: root,
@@ -119,7 +119,7 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
119119 })
120120 }
121121
122- f := newSwarm(false)
122+ f := newSupervisor(false)
123123 a, err := f.SpawnReq(context.Background(), SpawnRequest{Task: "x"})
124124 if err != nil {
125125 t.Fatalf("spawn: %v", err)
@@ -132,7 +132,7 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
132132 }
133133 a.Wait()
134134
135- g := newSwarm(true)
135+ g := newSupervisor(true)
136136 if loaded, errs := g.Reload(); loaded != 1 || len(errs) != 0 {
137137 t.Fatalf("reload loaded=%d errs=%v", loaded, errs)
138138 }
@@ -141,15 +141,15 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
141141 }
142142 }
143143
144-func TestSwarmAgentArgsOmitFastModeWhenUnset(t *testing.T) {
145- args := swarmAgentArgs(swarmAgentArgsOpts{Exe: "/zot", Dir: "/wt", SessionPath: "/s", InboxPath: "/i"})
144+func TestSubagentWorkerArgsOmitFastModeWhenUnset(t *testing.T) {
145+ args := subagentWorkerArgs(subagentWorkerArgsOpts{Exe: "/zot", Dir: "/wt", SessionPath: "/s", InboxPath: "/i"})
146146 if strings.Contains(strings.Join(args, " "), "--fast-mode") || strings.Contains(strings.Join(args, " "), "--no-fast-mode") {
147147 t.Fatalf("child args = %v, want fast mode omitted when unset", args)
148148 }
149149 }
150150
151-func TestSwarmAgentArgsExplicitlyDisablesFastMode(t *testing.T) {
152- args := swarmAgentArgs(swarmAgentArgsOpts{
151+func TestSubagentWorkerArgsExplicitlyDisablesFastMode(t *testing.T) {
152+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
153153 Exe: "/zot",
154154 Dir: "/wt",
155155 SessionPath: "/s",
R packages/agent/swarm/inbox.go → packages/agent/subagents/inbox.go +15 -38 renamed, 82% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "bufio"
@@ -12,28 +12,16 @@ import (
1212 "time"
1313 )
1414
15-// Inbox is the supervisor-side handle on a per-agent unix socket
16-// that the parent zot uses to send follow-up prompts (and other
17-// control lines) to a running swarm agent.
18-//
19-// Protocol on the wire: one UTF-8 message per line, newline-
20-// terminated. Three message kinds:
21-//
22-// user <text>... — append <text> as the next user turn
23-// cancel — cancel the agent's in-flight turn
24-// shutdown — graceful exit; child will write a final
25-// EvTurnEnd-equivalent JSON event and quit
26-//
27-// The child listens on the same socket via Listener (below) and
28-// translates the messages into core.Agent.Prompt calls. The
29-// parent never reads from the socket — it's a one-way channel
30-// driven by the supervisor.
15+// Inbox is the supervisor-side handle on a per-agent unix socket.
16+// The wire format is newline-delimited, versioned JSON commands. The child
17+// listens on the same socket via Listener (below); the parent never reads
18+// from it.
3119 //
3220 // Why a socket and not a FIFO: FIFOs are POSIX-only and have
3321 // awkward blocking-open semantics. A unix socket gives us:
3422 //
3523 // - the ability to fail fast if no child is listening yet,
36-// so SendInput returns a useful error to the TUI;
24+// so the supervisor gets a useful error to surface in the TUI;
3725 // - clean shutdown semantics when the child closes the
3826 // listener;
3927 // - works under tests with a temp dir; and
@@ -47,7 +35,7 @@ type Inbox struct {
4735 }
4836
4937 // NewInbox returns a handle that will dial the socket at path on
50-// the first SendInput call. The socket file is expected to be
38+// the first SendCommand call. The socket file is expected to be
5139 // created by the child (Listener.Open below). This split lets the
5240 // parent build the Inbox before the child has booted; sends that
5341 // happen before the child is ready get a clear "not yet" error
@@ -58,18 +46,7 @@ func NewInbox(path string) *Inbox { return &Inbox{path: path} }
5846 // pass the path to the child as a flag.
5947 func (b *Inbox) Path() string { return b.path }
6048
61-// SendInput writes msg to the inbox as one line. msg must already
62-// include the leading kind ("user ", "cancel", "shutdown") — the
63-// caller chooses the shape so we don't proliferate methods. The
64-// socket is reused across calls; transient dial failures yield
65-// ErrNotReady so the TUI can retry or report "agent not listening
66-// yet" rather than crashing.
67-func (b *Inbox) SendInput(msg string) error {
68- return b.sendBytes([]byte(msg + "\n"))
69-}
70-
71-// SendCommand sends a versioned JSONL command. SendInput remains available
72-// for older children and tests; new supervisor paths use this method.
49+// SendCommand sends a versioned JSONL command.
7350 func (b *Inbox) SendCommand(command Envelope) error {
7451 line, err := MarshalJSONL(command)
7552 if err != nil {
@@ -113,10 +90,10 @@ func (b *Inbox) Close() error {
11390 return nil
11491 }
11592
116-// ErrNotReady is returned by SendInput when the child hasn't yet
93+// ErrNotReady is returned when the child hasn't yet
11794 // opened its listener. Callers can retry after a short backoff;
11895 // the TUI surfaces it as "agent <id> not listening yet".
119-var ErrNotReady = errors.New("swarm: agent inbox not ready")
96+var ErrNotReady = errors.New("subagent: agent inbox not ready")
12097
12198 // dialUnix wraps net.DialTimeout with a small retry window so the
12299 // happy path "child just started, supervisor sends right away"
@@ -126,7 +103,7 @@ var ErrNotReady = errors.New("swarm: agent inbox not ready")
126103 // one is listening": either the socket file doesn't exist (child
127104 // hasn't booted) or it does exist but the dial was refused (the
128105 // previous child exited and left a stale inode). Both are
129-// recoverable by Swarm.Resume, and both should surface as the same
106+// recoverable by Supervisor.Resume, and both should surface as the same
130107 // user-facing hint rather than as a path-leaking error string.
131108 func dialUnix(path string, timeout time.Duration) (net.Conn, error) {
132109 deadline := time.Now().Add(timeout)
@@ -148,7 +125,7 @@ func dialUnix(path string, timeout time.Duration) (net.Conn, error) {
148125 if isNoListenerErr(lastErr) {
149126 return nil, ErrNotReady
150127 }
151- return nil, fmt.Errorf("swarm: dial %s: %w", path, lastErr)
128+ return nil, fmt.Errorf("subagent: dial %s: %w", path, lastErr)
152129 }
153130
154131 // isNoListenerErr reports whether err means "the socket file exists
@@ -165,7 +142,7 @@ func isNoListenerErr(err error) bool {
165142 // distinguish a live worker from a stale path. Callers use this probe before
166143 // resume/remove operations to avoid racing a detached worker's session and
167144 // workspace ownership.
168-const inboxProbeLine = "__zot_swarm_probe__"
145+const inboxProbeLine = "__zot_subagent_probe__"
169146
170147 func inboxLive(path string) bool {
171148 if path == "" {
@@ -184,7 +161,7 @@ func inboxLive(path string) bool {
184161 return writeErr == nil
185162 }
186163
187-// Listener is the child-side end of the inbox. The swarm-agent
164+// Listener is the child-side end of the inbox. The subagent-worker
188165 // daemon mode constructs one with Listen, then iterates Lines
189166 // to receive supervisor messages. Designed so a single goroutine
190167 // in the child can drive prompting without juggling raw net code.
@@ -217,7 +194,7 @@ func Listen(path string) (*Listener, error) {
217194 // could let Resume start a second worker on the same session and event log.
218195 if _, statErr := os.Stat(path); statErr == nil {
219196 if inboxLive(path) {
220- return nil, fmt.Errorf("swarm inbox already has a live listener")
197+ return nil, fmt.Errorf("subagent inbox already has a live listener")
221198 }
222199 // A refused probe means the socket inode is stale. Remove only that
223200 // endpoint; a live listener was handled above without unlinking it.
R packages/agent/swarm/inbox_test.go → packages/agent/subagents/inbox_test.go +16 -16 renamed, 86% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "errors"
@@ -20,7 +20,7 @@ import (
2020 func shortSocketDir(t *testing.T) string {
2121 t.Helper()
2222 if runtime.GOOS == "windows" {
23- t.Skip("swarm inbox transport uses Unix-domain sockets; Windows support will use a named-pipe backend")
23+ t.Skip("subagent inbox transport uses Unix-domain sockets; Windows support will use a named-pipe backend")
2424 }
2525 dir, err := os.MkdirTemp("/tmp", "zot-in-")
2626 if err != nil {
@@ -47,8 +47,8 @@ func TestInboxRoundTrip(t *testing.T) {
4747
4848 want := []string{"user hello", "user world", "cancel"}
4949 for _, msg := range want {
50- if err := inbox.SendInput(msg); err != nil {
51- t.Fatalf("SendInput(%q): %v", msg, err)
50+ if err := inbox.sendBytes([]byte(msg + "\n")); err != nil {
51+ t.Fatalf("sendBytes(%q): %v", msg, err)
5252 }
5353 }
5454
@@ -70,19 +70,19 @@ func TestInboxNotReady(t *testing.T) {
7070 path := filepath.Join(shortSocketDir(t), "in.sock")
7171 inbox := NewInbox(path)
7272 defer inbox.Close()
73- err := inbox.SendInput("user hi")
73+ err := inbox.sendBytes([]byte("user hi\n"))
7474 if err == nil {
75- t.Fatal("SendInput on missing socket: want error")
75+ t.Fatal("sendBytes on missing socket: want error")
7676 }
7777 if !errors.Is(err, ErrNotReady) {
78- t.Fatalf("SendInput err = %v; want ErrNotReady", err)
78+ t.Fatalf("sendBytes err = %v; want ErrNotReady", err)
7979 }
8080 }
8181
8282 // TestIsNoListenerErrMatchesECONNREFUSED documents the contract
8383 // that the user's screenshot regression depends on: any dial error
8484 // whose root cause is "the kernel refused the connect because nobody
85-// is listening" must fold into ErrNotReady at the SendInput layer,
85+// is listening" must fold into ErrNotReady at the sendBytes layer,
8686 // so the TUI shows "agent not accepting input (press R to resume)"
8787 // instead of leaking the unix-socket path. We pattern-match via
8888 // errors.Is on syscall.ECONNREFUSED rather than string-matching the
@@ -104,7 +104,7 @@ func TestIsNoListenerErrMatchesECONNREFUSED(t *testing.T) {
104104 }
105105
106106 // TestInboxNewSupervisorPreemptsOld documents that only one
107-// supervisor connection at a time is honoured. The swarm design
107+// supervisor connection at a time is honoured. The subagent design
108108 // assumes a single parent zot owns each agent; if a second
109109 // parent dials, the listener boots the first one. The first
110110 // supervisor's already-delivered messages still land in the
@@ -119,7 +119,7 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) {
119119 defer ln.Close()
120120
121121 first := NewInbox(path)
122- if err := first.SendInput("user one"); err != nil {
122+ if err := first.sendBytes([]byte("user one\n")); err != nil {
123123 t.Fatalf("first send: %v", err)
124124 }
125125 // Wait for the listener to absorb "user one" before dialing the
@@ -130,7 +130,7 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) {
130130
131131 second := NewInbox(path)
132132 defer second.Close()
133- if err := second.SendInput("user two"); err != nil {
133+ if err := second.sendBytes([]byte("user two\n")); err != nil {
134134 t.Fatalf("second send: %v", err)
135135 }
136136
@@ -140,17 +140,17 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) {
140140 }
141141
142142 // The original supervisor's connection was preempted by the
143- // listener accepting the second dial. The next SendInput on the
143+ // listener accepting the second dial. The next sendBytes on the
144144 // first handle sees a broken pipe and drops the cached connection;
145145 // the call after that redials successfully. Both behaviours
146146 // matter: the broken-pipe error must surface (so a real
147147 // supervisor knows it lost ownership) AND the next call must
148148 // recover so a flaky reconnect doesn't permanently wedge the
149149 // inbox.
150- if err := first.SendInput("user three"); err == nil {
150+ if err := first.sendBytes([]byte("user three\n")); err == nil {
151151 t.Fatal("expected broken-pipe error on stale connection")
152152 }
153- if err := first.SendInput("user four"); err != nil {
153+ if err := first.sendBytes([]byte("user four\n")); err != nil {
154154 t.Fatalf("first redial after eviction: %v", err)
155155 }
156156 }
@@ -197,7 +197,7 @@ func collectFor(t *testing.T, ch <-chan string, n int, timeout time.Duration) <-
197197 return out
198198 }
199199
200-// TestInboxConcurrentSends serialises the SendInput call so two
200+// TestInboxConcurrentSends serialises the sendBytes call so two
201201 // goroutines writing at once still produce two whole lines, never
202202 // a tearing of bytes between them.
203203 func TestInboxConcurrentSends(t *testing.T) {
@@ -216,7 +216,7 @@ func TestInboxConcurrentSends(t *testing.T) {
216216 for i := 0; i < n; i++ {
217217 go func(i int) {
218218 defer wg.Done()
219- if err := inbox.SendInput("user msg"); err != nil {
219+ if err := inbox.sendBytes([]byte("user msg\n")); err != nil {
220220 t.Errorf("send %d: %v", i, err)
221221 }
222222 }(i)
R packages/agent/swarm/lease.go → packages/agent/subagents/lease.go +3 -3 renamed, 80% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "errors"
@@ -8,8 +8,8 @@ import (
88 )
99
1010 // ErrAgentLeaseHeld means another supervisor currently owns an agent's
11-// durable state. The lease is advisory only to old versions; socket probing
12-// remains the compatibility fence for workers that predate the lease.
11+// durable state. The lease is advisory; socket probing remains the fence for
12+// workers that are still live.
1313 var ErrAgentLeaseHeld = errors.New("subagents: agent state is owned by another supervisor")
1414
1515 type agentLease struct {
R packages/agent/swarm/lease_test.go → packages/agent/subagents/lease_test.go +1 -1 renamed, 97% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "errors"
R packages/agent/swarm/lease_unix.go → packages/agent/subagents/lease_unix.go +1 -1 renamed, 97% similar
@@ -1,6 +1,6 @@
11 //go:build !windows
22
3-package swarm
3+package subagents
44
55 import (
66 "errors"
R packages/agent/swarm/lease_windows.go → packages/agent/subagents/lease_windows.go +1 -1 renamed, 97% similar
@@ -1,6 +1,6 @@
11 //go:build windows
22
3-package swarm
3+package subagents
44
55 import (
66 "errors"
R packages/agent/swarm/model.go → packages/agent/subagents/model.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import "time"
44
R packages/agent/swarm/persist.go → packages/agent/subagents/persist.go +40 -52 renamed, 90% similar
@@ -1,11 +1,11 @@
1-package swarm
1+package subagents
22
3-// On-disk persistence for swarm agents.
3+// On-disk persistence for subagent agents.
44 //
55 // Every Spawn writes a meta.json next to the agent's events.jsonl and
66 // session.json. The file captures the immutable identity bits (id,
77 // task, branch, dir) plus the paths the runner needs to resume the
8-// agent later. On a fresh zot launch, Swarm.Reload() walks
8+// agent later. On a fresh zot launch, Supervisor.Reload() walks
99 // <root>/agents/*/meta.json and re-registers every agent it finds in
1010 // StatusDetached so the user can see, view, resume, or remove them
1111 // from the dashboard.
@@ -31,8 +31,8 @@ import (
3131
3232 // agentMeta is the durable identity record for one agent. Only fields
3333 // the supervisor needs to rebuild an Agent after a restart live here.
34-// Adding a field is backwards-compatible (older meta.json files just
35-// leave it zero); removing or renaming one is not.
34+// Adding a field leaves it zero when absent; removing or renaming a field
35+// changes the persisted contract.
3636 //
3737 // Historical fields like `branch` and `isolated` are silently dropped
3838 // by encoding/json's permissive decoder when an older meta.json is
@@ -81,7 +81,7 @@ type agentMeta struct {
8181 // (and agents spawned outside of any session, e.g. by tests or
8282 // scripted callers that didn't call SetActiveSession) have an
8383 // empty SessionID and are visible from every session as a
84- // backward-compat fallback.
84+ // unscoped-agent fallback.
8585 SessionID string `json:"session_id,omitempty"`
8686 }
8787
@@ -145,7 +145,7 @@ func writeAgentMeta(stateDir string, a *Agent) error {
145145 }
146146 b, err := json.MarshalIndent(m, "", " ")
147147 if err != nil {
148- return fmt.Errorf("swarm meta marshal: %w", err)
148+ return fmt.Errorf("subagent meta marshal: %w", err)
149149 }
150150 if err := os.MkdirAll(stateDir, 0o700); err != nil {
151151 return fmt.Errorf("subagents meta dir: %w", err)
@@ -157,7 +157,7 @@ func writeAgentMeta(stateDir string, a *Agent) error {
157157 }
158158 if err := os.Rename(tmp, final); err != nil {
159159 _ = os.Remove(tmp)
160- return fmt.Errorf("swarm meta rename: %w", err)
160+ return fmt.Errorf("subagent meta rename: %w", err)
161161 }
162162 return nil
163163 }
@@ -172,10 +172,10 @@ func readAgentMeta(stateDir string) (agentMeta, error) {
172172 return m, err
173173 }
174174 if err := json.Unmarshal(b, &m); err != nil {
175- return m, fmt.Errorf("swarm meta parse %s: %w", stateDir, err)
175+ return m, fmt.Errorf("subagent meta parse %s: %w", stateDir, err)
176176 }
177177 if m.ID == "" {
178- return m, fmt.Errorf("swarm meta %s: missing id", stateDir)
178+ return m, fmt.Errorf("subagent meta %s: missing id", stateDir)
179179 }
180180 return m, nil
181181 }
@@ -188,19 +188,19 @@ func readAgentMeta(stateDir string) (agentMeta, error) {
188188 //
189189 // The reloaded agents have no live Runner; the user can:
190190 // - view their transcript (the dashboard reads from EventLogPath),
191-// - resume them via Swarm.Resume (starts a fresh subprocess on the
191+// - resume them via Supervisor.Resume (starts a fresh subprocess on the
192192 // same worktree / session / inbox path),
193193 // - remove them (worktree + meta + events log gone).
194194 //
195195 // Reload returns the number of agents loaded plus any per-directory
196196 // error encountered. Malformed entries are skipped rather than
197197 // failing the whole reload — one bad meta.json shouldn't hide the
198-// rest of the swarm.
198+// rest of the subagents.
199199 func safeAgentID(id string) bool {
200200 return id != "" && id != "." && id != ".." && filepath.Base(id) == id && !strings.ContainsAny(id, `/\\`)
201201 }
202202
203-func (f *Swarm) sanitizeReloadMeta(stateDir, root string, m *agentMeta) error {
203+func (f *Supervisor) sanitizeReloadMeta(stateDir, root string, m *agentMeta) error {
204204 if m == nil {
205205 return errors.New("subagents reload: nil metadata")
206206 }
@@ -234,7 +234,7 @@ func validRuntimeInboxPath(path, id string) bool {
234234 return false
235235 }
236236 dir := filepath.Dir(path)
237- if !strings.HasPrefix(filepath.Base(dir), "zot-swarm-") {
237+ if !strings.HasPrefix(filepath.Base(dir), "zot-subagents-") {
238238 return false
239239 }
240240 bases := []string{os.TempDir(), "/tmp"}
@@ -258,28 +258,17 @@ func firstNonEmpty(values ...string) string {
258258 return ""
259259 }
260260
261-func (f *Swarm) Reload() (loaded int, errs []error) {
262- roots := []string{f.cfg.Root}
263- roots = append(roots, f.cfg.LegacyRoots...)
264- seenRoots := make(map[string]struct{}, len(roots))
265- for _, root := range roots {
266- root = filepath.Clean(root)
267- if root == "." {
268- continue
269- }
270- if _, seen := seenRoots[root]; seen {
271- continue
272- }
273- seenRoots[root] = struct{}{}
274- count, rootErrs := f.reloadRoot(root)
275- loaded += count
276- errs = append(errs, rootErrs...)
277- errs = append(errs, f.reloadBatches(root)...)
261+func (f *Supervisor) Reload() (loaded int, errs []error) {
262+ root := filepath.Clean(f.cfg.Root)
263+ if root == "." {
264+ return 0, nil
278265 }
266+ loaded, errs = f.reloadRoot(root)
267+ errs = append(errs, f.reloadBatches(root)...)
279268 return loaded, errs
280269 }
281270
282-func (f *Swarm) reloadRoot(root string) (loaded int, errs []error) {
271+func (f *Supervisor) reloadRoot(root string) (loaded int, errs []error) {
283272 agentsDir := filepath.Join(root, "agents")
284273 entries, err := os.ReadDir(agentsDir)
285274 if err != nil {
@@ -338,15 +327,14 @@ func (f *Swarm) reloadRoot(root string) (loaded int, errs []error) {
338327 //
339328 // The returned Agent has a closed `done` channel because Wait should
340329 // return instantly: there is nothing to wait for.
341-func (f *Swarm) buildDetachedAgent(m agentMeta) *Agent {
342- // Legacy meta.json files may not record a repository identity. Those
343- // records are migrated to the current RepoRoot; current-format records
344- // retain their persisted repository so a restart from another checkout
345- // cannot silently redirect the child.
330+func (f *Supervisor) buildDetachedAgent(m agentMeta) *Agent {
331+ // Metadata without a repository identity uses the current RepoRoot;
332+ // records with one retain their persisted repository so a restart from
333+ // another checkout cannot silently redirect the child.
346334 dir := m.Dir
347335 if m.WorkspaceMode != WorkspaceWorktree && m.RepositoryRoot == "" && f.cfg.RepoRoot != "" {
348- // Legacy metadata did not record the repository identity. Preserve
349- // the historical best-effort migration only for those records.
336+ // Preserve the best-effort migration only for records without an
337+ // explicit repository identity.
350338 dir = f.cfg.RepoRoot
351339 }
352340 if dir == "" {
@@ -569,32 +557,32 @@ func replayEventsIntoAgent(a *Agent, evs []Event) {
569557 // Resume re-attaches a Runner to a previously-spawned agent. Durable
570558 // session and event files are kept, while the transient inbox path is
571559 // recalculated for the current runtime environment. Use this to
572-// continue a swarm session across zot restarts:
560+// continue a subagent session across zot restarts:
573561 //
574-// swarmMgr.Reload()
575-// a, err := swarmMgr.Resume(ctx, "alpha-12345")
576-// swarmMgr.SendUserTurn(a.ID, "where were we?")
562+// subagentMgr.Reload()
563+// a, err := subagentMgr.Resume(ctx, "alpha-12345")
564+// subagentMgr.SendUserTurn(a.ID, "where were we?")
577565 //
578566 // The agent must be in a non-running state (Detached, Done, Failed,
579567 // Killed). Resuming a still-running agent returns an error so two
580568 // runners don't race for the same session.
581-func (f *Swarm) Resume(ctx context.Context, id string) (*Agent, error) {
569+func (f *Supervisor) Resume(ctx context.Context, id string) (*Agent, error) {
582570 return f.resume(ctx, id, true)
583571 }
584572
585573 // ResumeSession continues the existing child session without replaying the
586574 // original task. It is the explicit canonical spelling for Resume.
587-func (f *Swarm) ResumeSession(ctx context.Context, id string) (*Agent, error) {
575+func (f *Supervisor) ResumeSession(ctx context.Context, id string) (*Agent, error) {
588576 return f.resume(ctx, id, true)
589577 }
590578
591579 // RestartTask intentionally runs the stored original task again in a fresh
592580 // worker attempt. Callers should use ResumeSession for normal recovery.
593-func (f *Swarm) RestartTask(ctx context.Context, id string) (*Agent, error) {
581+func (f *Supervisor) RestartTask(ctx context.Context, id string) (*Agent, error) {
594582 return f.resume(ctx, id, false)
595583 }
596584
597-func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, error) {
585+func (f *Supervisor) resume(ctx context.Context, id string, resuming bool) (*Agent, error) {
598586 f.operationMu.Lock()
599587 defer f.operationMu.Unlock()
600588 if ctx == nil {
@@ -602,25 +590,25 @@ func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, e
602590 }
603591 existing := f.Get(id)
604592 if existing == nil {
605- return nil, fmt.Errorf("swarm: no such agent %q", id)
593+ return nil, fmt.Errorf("subagent: no such agent %q", id)
606594 }
607595 existing.mu.Lock()
608596 st := existing.status
609597 existing.mu.Unlock()
610598 if st == StatusRunning || st == StatusPending {
611- return nil, fmt.Errorf("swarm: agent %s is still %s; stop it first", existing.ID, st)
599+ return nil, fmt.Errorf("subagent: agent %s is still %s; stop it first", existing.ID, st)
612600 }
613601 select {
614602 case <-existing.done:
615603 default:
616- return nil, fmt.Errorf("swarm: agent %s is still finalizing; wait before resuming", existing.ID)
604+ return nil, fmt.Errorf("subagent: agent %s is still finalizing; wait before resuming", existing.ID)
617605 }
618606 // A reloaded worker may still be alive even though this supervisor has no
619607 // in-process Runner (the old host can disappear after the child daemon
620608 // starts). Fence resume before touching the shared session/worktree: a
621609 // second worker must never own the same inbox and files concurrently.
622610 if inboxLive(existing.InboxPath) {
623- return nil, fmt.Errorf("swarm: agent %s still has a live worker; send shutdown or stop it first", existing.ID)
611+ return nil, fmt.Errorf("subagent: agent %s still has a live worker; send shutdown or stop it first", existing.ID)
624612 }
625613 stateDir := existing.stateDirectory(f.cfg.Root)
626614 lease, err := acquireAgentLease(stateDir)
@@ -641,7 +629,7 @@ func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, e
641629 // older zot version, so always select it again on resume.
642630 inboxPath, err := inboxSocketPath(f.cfg.Root, existing.ID)
643631 if err != nil {
644- return nil, fmt.Errorf("swarm inbox path: %w", err)
632+ return nil, fmt.Errorf("subagent inbox path: %w", err)
645633 }
646634 fastMode := existing.FastMode
647635 m := agentMeta{
R packages/agent/swarm/persist_test.go → packages/agent/subagents/persist_test.go +33 -33 renamed, 94% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -64,7 +64,7 @@ func TestSpawnWritesMetaJSON(t *testing.T) {
6464 }
6565
6666 // TestReloadRebuildsDetachedAgents simulates a zot restart by spawning
67-// in one Swarm, throwing it away, then opening a fresh Swarm against
67+// in one Supervisor, throwing it away, then opening a fresh Supervisor against
6868 // the same root and calling Reload. The user-visible state — id,
6969 // task, branch, dir — must come back, and status must be Detached so
7070 // the dashboard can offer resume.
@@ -240,7 +240,7 @@ func TestReloadIsIdempotent(t *testing.T) {
240240 // TestReloadReplaysTranscriptFromEventLog drops a curated events.jsonl
241241 // next to a meta.json and checks that the transcript surfaces in the
242242 // reloaded agent. This is the user-facing payoff of Reload: opening
243-// /swarm logs <id> after a restart shows what the agent said before.
243+// /subagents logs <id> after a restart shows what the agent said before.
244244 func TestReloadReplaysTranscriptFromEventLog(t *testing.T) {
245245 root := t.TempDir()
246246 id := "alpha-9"
@@ -423,9 +423,9 @@ func TestResumeRestartsRunnerOnSameSession(t *testing.T) {
423423 }
424424 }
425425
426-func TestResumeRecalculatesLegacyStateInboxPath(t *testing.T) {
426+func TestResumeRecalculatesInvalidStateInboxPath(t *testing.T) {
427427 if runtime.GOOS == "windows" {
428- t.Skip("swarm inbox runtime relocation applies to Unix sockets")
428+ t.Skip("subagent inbox runtime relocation applies to Unix sockets")
429429 }
430430 root := t.TempDir()
431431 f := New(Config{
@@ -434,21 +434,21 @@ func TestResumeRecalculatesLegacyStateInboxPath(t *testing.T) {
434434 return RunnerFunc(func(context.Context, Sink) error { return nil })
435435 },
436436 })
437- a, err := f.Spawn(context.Background(), "legacy inbox")
437+ a, err := f.Spawn(context.Background(), "invalid inbox")
438438 if err != nil {
439439 t.Fatal(err)
440440 }
441441 a.Wait()
442442
443- legacyPath := filepath.Join(root, "agents", a.ID, "in.sock")
444- a.InboxPath = legacyPath
443+ invalidPath := filepath.Join(root, "agents", a.ID, "in.sock")
444+ a.InboxPath = invalidPath
445445 a2, err := f.Resume(context.Background(), a.ID)
446446 if err != nil {
447447 t.Fatalf("resume: %v", err)
448448 }
449449 a2.Wait()
450- if a2.InboxPath == legacyPath {
451- t.Fatalf("resume kept legacy state-directory inbox path %q", legacyPath)
450+ if a2.InboxPath == invalidPath {
451+ t.Fatalf("resume kept state-directory inbox path %q", invalidPath)
452452 }
453453 if strings.HasPrefix(a2.InboxPath, root+string(filepath.Separator)) {
454454 t.Fatalf("resumed inbox %q remains below state root %q", a2.InboxPath, root)
@@ -557,7 +557,7 @@ func TestResumeAfterReload(t *testing.T) {
557557 a.StopAll()
558558 ag.Wait()
559559
560- // Process B: a new Swarm against the same root.
560+ // Process B: a new Supervisor against the same root.
561561 resumed := make(chan struct{}, 1)
562562 b := New(Config{
563563 Root: root, RepoRoot: root,
@@ -667,9 +667,9 @@ func TestResumeFencesLiveReloadedWorker(t *testing.T) {
667667 }
668668 }
669669
670-func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) {
670+func TestSupervisorProviderSettingsPersistAcrossReloadAndResume(t *testing.T) {
671671 root := t.TempDir()
672- newSwarm := func(baseURL string, insecureTLS bool) *Swarm {
672+ newSupervisor := func(baseURL string, insecureTLS bool) *Supervisor {
673673 return New(Config{
674674 Root: root,
675675 RepoRoot: root,
@@ -684,7 +684,7 @@ func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) {
684684 })
685685 }
686686
687- first := newSwarm("https://parent.example.test/v1", true)
687+ first := newSupervisor("https://parent.example.test/v1", true)
688688 a, err := first.SpawnReq(context.Background(), SpawnRequest{Task: "x"})
689689 if err != nil {
690690 t.Fatalf("spawn: %v", err)
@@ -715,7 +715,7 @@ func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) {
715715 t.Fatalf("metadata contains credential material: %s", metaBytes)
716716 }
717717
718- second := newSwarm("https://different.example.test/v1", false)
718+ second := newSupervisor("https://different.example.test/v1", false)
719719 if loaded, errs := second.Reload(); loaded != 1 || len(errs) != 0 {
720720 t.Fatalf("reload loaded=%d errs=%v", loaded, errs)
721721 }
@@ -823,7 +823,7 @@ func TestSpawnReqPersistsModel(t *testing.T) {
823823 t.Errorf("meta = (%q,%q,%q,%q,%q); want model/provider/reasoning/profile/session persisted", got.Model, got.Provider, got.Reasoning, got.Subagent, got.SessionID)
824824 }
825825
826- // Reload in a fresh Swarm and confirm the detached agent still
826+ // Reload in a fresh Supervisor and confirm the detached agent still
827827 // carries the model/provider so Resume can route the child
828828 // subprocess back to the same model.
829829 g := New(Config{
@@ -856,12 +856,12 @@ func TestSpawnReqPersistsModel(t *testing.T) {
856856 resumed.Wait()
857857 }
858858
859-// TestSwarmAgentArgsIncludesModelFlags pins the argv contract that
859+// TestSubagentWorkerArgsIncludesModelFlags pins the argv contract that
860860 // connects the supervisor's per-agent model override to the child's
861861 // --model / --provider flag set. Adding a new path to argv without
862862 // updating this assertion is the failure mode this catches.
863-func TestSwarmAgentArgsIncludesModelFlags(t *testing.T) {
864- args := swarmAgentArgs(swarmAgentArgsOpts{
863+func TestSubagentWorkerArgsIncludesModelFlags(t *testing.T) {
864+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
865865 Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock",
866866 Task: "do x", Model: "gpt-5", Provider: "openai",
867867 })
@@ -887,14 +887,14 @@ func TestSwarmAgentArgsIncludesModelFlags(t *testing.T) {
887887
888888 // TestStopOnDetachedAgentIsNoopAndDoesNotPanic regression-tests the
889889 // segfault that hit when the user pressed 'k' on a reloaded
890-// (detached) agent: Swarm.Stop unconditionally called a.cancel(),
890+// (detached) agent: Supervisor.Stop unconditionally called a.cancel(),
891891 // but buildDetachedAgent never assigns a cancel func because there's
892892 // no in-process runner to cancel. The fix is to short-circuit Stop
893893 // for StatusDetached and — belt-and-braces — nil-check a.cancel.
894894 func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) {
895895 root := t.TempDir()
896896 // Build a detached agent the same way Reload does: drop a
897- // meta.json on disk, then ask a fresh Swarm to pick it up.
897+ // meta.json on disk, then ask a fresh Supervisor to pick it up.
898898 id := "alpha-1"
899899 stateDir := filepath.Join(root, "agents", id)
900900 if err := os.MkdirAll(stateDir, 0o755); err != nil {
@@ -919,7 +919,7 @@ func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) {
919919 }
920920 a := f.Get(id)
921921 if a == nil {
922- t.Fatal("detached agent missing from swarm")
922+ t.Fatal("detached agent missing from subagent")
923923 }
924924 if a.Status() != StatusDetached {
925925 t.Fatalf("setup: agent status = %q; want detached", a.Status())
@@ -973,7 +973,7 @@ func TestRemoveAlsoCleansStateDir(t *testing.T) {
973973 t.Fatalf("state dir should be gone after remove; got %v", err)
974974 }
975975
976- // A fresh Swarm + Reload should find nothing.
976+ // A fresh Supervisor + Reload should find nothing.
977977 g := New(Config{Root: root, RepoRoot: root})
978978 if loaded, _ := g.Reload(); loaded != 0 {
979979 t.Fatalf("reload after remove loaded=%d; want 0", loaded)
@@ -1012,7 +1012,7 @@ func TestRemoveReloadedAgentKeepsSiblingState(t *testing.T) {
10121012 }
10131013
10141014 // TestActiveSessionScopesSnapshotAll proves the session filter the
1015-// user asked for: a single Swarm with agents spawned under two
1015+// user asked for: a single Supervisor with agents spawned under two
10161016 // different active sessions only surfaces the agents matching the
10171017 // currently-active session via SnapshotAll. Switching the active
10181018 // session re-narrows the view without touching agent state.
@@ -1074,14 +1074,14 @@ func TestActiveSessionScopesSnapshotAll(t *testing.T) {
10741074 }
10751075
10761076 // TestSessionIDPersistsAcrossReload confirms the SessionID field
1077-// survives a Swarm restart: an agent spawned under session A in one
1078-// Swarm instance is still SessionID="sess-A" after a fresh New +
1077+// survives a Supervisor restart: an agent spawned under session A in one
1078+// Supervisor instance is still SessionID="sess-A" after a fresh New +
10791079 // Reload reads it back from meta.json. Without persistence, the
10801080 // scope filter would forget which session owned each agent after
10811081 // a zot restart and the dashboard would leak everything again.
10821082 func TestSessionIDPersistsAcrossReload(t *testing.T) {
10831083 root := t.TempDir()
1084- mkSwarm := func() *Swarm {
1084+ mkSupervisor := func() *Supervisor {
10851085 return New(Config{
10861086 Root: root, RepoRoot: root,
10871087 NewRunner: func(a *Agent) Runner {
@@ -1090,7 +1090,7 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) {
10901090 })
10911091 }
10921092
1093- f := mkSwarm()
1093+ f := mkSupervisor()
10941094 f.SetActiveSession("sess-keep")
10951095 a, err := f.Spawn(context.Background(), "persist me")
10961096 if err != nil {
@@ -1099,8 +1099,8 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) {
10991099 _ = f.Stop(a.ID)
11001100 a.Wait()
11011101
1102- // Fresh Swarm, same root, reload from disk.
1103- g := mkSwarm()
1102+ // Fresh Supervisor, same root, reload from disk.
1103+ g := mkSupervisor()
11041104 if loaded, errs := g.Reload(); loaded != 1 || len(errs) > 0 {
11051105 t.Fatalf("reload loaded=%d errs=%v; want 1 / no errs", loaded, errs)
11061106 }
@@ -1118,7 +1118,7 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) {
11181118 }
11191119 }
11201120
1121-// TestEmptySessionIDIsVisibleFromAnyScope pins the backward-compat
1121+// TestEmptySessionIDIsVisibleFromAnyScope pins the unscoped-agent
11221122 // rule: agents whose meta.json predates the SessionID field (or who
11231123 // were spawned without an active session, e.g. via a test rig or
11241124 // scripted caller) carry SessionID == "" and remain visible from
@@ -1133,7 +1133,7 @@ func TestEmptySessionIDIsVisibleFromAnyScope(t *testing.T) {
11331133 },
11341134 })
11351135 // No SetActiveSession call → agent spawned with empty SessionID.
1136- a, err := f.Spawn(context.Background(), "legacy")
1136+ a, err := f.Spawn(context.Background(), "unscoped")
11371137 if err != nil {
11381138 t.Fatal(err)
11391139 }
@@ -1145,7 +1145,7 @@ func TestEmptySessionIDIsVisibleFromAnyScope(t *testing.T) {
11451145 f.SetActiveSession(scope)
11461146 got := snapshotIDs(f.SnapshotAll())
11471147 if len(got) != 1 || got[0] != a.ID {
1148- t.Errorf("scope=%q: ids = %v; want legacy agent visible", scope, got)
1148+ t.Errorf("scope=%q: ids = %v; want unscoped agent visible", scope, got)
11491149 }
11501150 }
11511151
R packages/agent/swarm/process_unix.go → packages/agent/subagents/process_unix.go +1 -1 renamed, 98% similar
@@ -1,6 +1,6 @@
11 //go:build !windows
22
3-package swarm
3+package subagents
44
55 import (
66 "os/exec"
R packages/agent/swarm/process_unix_test.go → packages/agent/subagents/process_unix_test.go +1 -1 renamed, 98% similar
@@ -1,6 +1,6 @@
11 //go:build !windows
22
3-package swarm
3+package subagents
44
55 import (
66 "context"
R packages/agent/swarm/process_windows.go → packages/agent/subagents/process_windows.go +1 -1 renamed, 98% similar
@@ -1,6 +1,6 @@
11 //go:build windows
22
3-package swarm
3+package subagents
44
55 import (
66 "os/exec"
M packages/agent/subagents/profiles.go +2 -2
@@ -1,5 +1,5 @@
11 // Package subagents discovers named agent profiles that can be selected by
2-// zot's swarm supervisor. Profiles use the common markdown/frontmatter layout:
2+// zot's subagent supervisor. Profiles use the common markdown/frontmatter layout:
33 // a YAML-like metadata block followed by the agent's system prompt.
44 //
55 // Discovery prefers explicitly configured directories, then the shared
@@ -132,7 +132,7 @@ func SystemPromptAddendum(profiles []*Profile) string {
132132 }
133133 var sb strings.Builder
134134 sb.WriteString("[subagents_list]\n")
135- sb.WriteString("Named subagents available to swarm_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, tool limits, and fast-mode preference are applied to the child.\n")
135+ sb.WriteString("Named subagents available to subagent_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, tool limits, and fast-mode preference are applied to the child.\n")
136136 for _, profile := range profiles {
137137 if profile == nil {
138138 continue
R packages/agent/swarm/protocol.go → packages/agent/subagents/protocol.go +47 -278 renamed, 66% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "bufio"
@@ -14,15 +14,8 @@ import (
1414 )
1515
1616 // ProtocolVersion is the version of the supervisor/worker JSONL protocol.
17-// Version zero is reserved for the legacy flat event and text-inbox formats;
18-// it is not a versioned wire format.
1917 const ProtocolVersion = 1
2018
21-// LegacyProtocolVersion identifies a message read from the pre-versioned
22-// protocol. Legacy messages are accepted by the readers but new messages are
23-// always emitted at ProtocolVersion.
24-const LegacyProtocolVersion = 0
25-
2619 // Canonical parent-to-worker command names.
2720 const (
2821 CommandAgentPing = "agent.ping"
@@ -67,14 +60,14 @@ const (
6760
6861 var (
6962 // ErrEmptyProtocolLine is returned for a blank JSONL line or command.
70- ErrEmptyProtocolLine = errors.New("swarm protocol: empty line")
63+ ErrEmptyProtocolLine = errors.New("subagent protocol: empty line")
7164 // ErrNotCommand is returned when ParseCommand is given a JSON event.
72- ErrNotCommand = errors.New("swarm protocol: message is not a command")
65+ ErrNotCommand = errors.New("subagent protocol: message is not a command")
7366 // ErrNotEvent is returned when ParseEvent is given a JSON command.
74- ErrNotEvent = errors.New("swarm protocol: message is not an event")
67+ ErrNotEvent = errors.New("subagent protocol: message is not an event")
7568 // ErrUnsupportedProtocolVersion is returned by Validate for a version
7669 // this implementation cannot interpret.
77- ErrUnsupportedProtocolVersion = errors.New("swarm protocol: unsupported version")
70+ ErrUnsupportedProtocolVersion = errors.New("subagent protocol: unsupported version")
7871 )
7972
8073 var commandNames = map[string]struct{}{
@@ -146,8 +139,7 @@ func IsMessageID(id string) bool {
146139 // "turn_id":"...","timestamp":"...","payload":{...}}
147140 //
148141 // TurnID is omitted when it is empty. Unknown top-level fields are retained in
149-// Unknown. A flat legacy event is represented by Legacy=true and uses `time`
150-// instead of `timestamp` when marshaled.
142+// Unknown.
151143 type Envelope struct {
152144 Version int `json:"version"`
153145 Type string `json:"type"`
@@ -160,18 +152,6 @@ type Envelope struct {
160152 // Unknown contains unrecognised top-level fields from a versioned
161153 // envelope. Payload itself retains unrecognised payload fields.
162154 Unknown map[string]json.RawMessage `json:"-"`
163-
164- // Legacy is true only for an old flat event read by ParseEnvelope. It
165- // causes MarshalJSON to retain the old flat shape. It is not set for a
166- // text inbox command, since text commands are normalised to a new
167- // versioned command as soon as they are read.
168- Legacy bool `json:"-"`
169-
170- // LegacyCommand and LegacyText record that this envelope came from a
171- // text inbox command. They are useful to bridges that need to echo the
172- // original command, and do not affect versioned JSON marshaling.
173- LegacyCommand bool `json:"-"`
174- LegacyText string `json:"-"`
175155 }
176156
177157 // Command and ProtocolEvent are aliases rather than separate structs so a
@@ -212,11 +192,11 @@ func NewEnvelope(typ, agentID, turnID string, payload ...any) Envelope {
212192 // NewEnvelopeWithPayload is the checked form of NewEnvelope.
213193 func NewEnvelopeWithPayload(typ, agentID, turnID string, payload any) (Envelope, error) {
214194 if strings.TrimSpace(typ) == "" {
215- return Envelope{}, errors.New("swarm protocol: empty message type")
195+ return Envelope{}, errors.New("subagent protocol: empty message type")
216196 }
217197 p, err := marshalPayload(payload)
218198 if err != nil {
219- return Envelope{}, fmt.Errorf("swarm protocol payload: %w", err)
199+ return Envelope{}, fmt.Errorf("subagent protocol payload: %w", err)
220200 }
221201 return Envelope{
222202 Version: ProtocolVersion,
@@ -242,7 +222,7 @@ func NewEventEnvelope(name, agentID, turnID string, payload ...any) Envelope {
242222 }
243223
244224 // NewProtocolEvent is a more explicit spelling of NewEventEnvelope. NewEvent
245-// is already the legacy flat-event constructor in this package.
225+// is the canonical event-envelope constructor in this package.
246226 func NewProtocolEvent(name, agentID, turnID string, payload ...any) Envelope {
247227 return NewEventEnvelope(name, agentID, turnID, payload...)
248228 }
@@ -279,29 +259,23 @@ func marshalPayload(payload any) (json.RawMessage, error) {
279259 // intentionally separate from ParseEnvelope: parsing an unknown future event
280260 // should succeed, while a sender can opt into strict validation before write.
281261 func (e Envelope) Validate() error {
282- if e.Legacy {
283- if e.Type == "" {
284- return errors.New("swarm protocol: legacy event has no type")
285- }
286- return nil
287- }
288262 if e.Version != ProtocolVersion {
289263 return fmt.Errorf("%w: %d", ErrUnsupportedProtocolVersion, e.Version)
290264 }
291265 if e.Type == "" {
292- return errors.New("swarm protocol: missing type")
266+ return errors.New("subagent protocol: missing type")
293267 }
294268 if e.MessageID == "" {
295- return errors.New("swarm protocol: missing message_id")
269+ return errors.New("subagent protocol: missing message_id")
296270 }
297271 if e.AgentID == "" {
298- return errors.New("swarm protocol: missing agent_id")
272+ return errors.New("subagent protocol: missing agent_id")
299273 }
300274 if e.Timestamp.IsZero() {
301- return errors.New("swarm protocol: missing timestamp")
275+ return errors.New("subagent protocol: missing timestamp")
302276 }
303277 if len(e.Payload) == 0 || !json.Valid(e.Payload) {
304- return errors.New("swarm protocol: invalid payload")
278+ return errors.New("subagent protocol: invalid payload")
305279 }
306280 return nil
307281 }
@@ -317,10 +291,10 @@ func (e Envelope) IsEvent() bool { return IsEventName(e.Type) }
317291 // event while still retaining it for a later forward-compatible write.
318292 func (e Envelope) DecodePayload(dst any) error {
319293 if dst == nil {
320- return errors.New("swarm protocol: nil payload destination")
294+ return errors.New("subagent protocol: nil payload destination")
321295 }
322296 if len(e.Payload) == 0 {
323- return errors.New("swarm protocol: missing payload")
297+ return errors.New("subagent protocol: missing payload")
324298 }
325299 return json.Unmarshal(e.Payload, dst)
326300 }
@@ -333,7 +307,7 @@ func (e Envelope) PayloadFields() (map[string]json.RawMessage, error) {
333307 return nil, err
334308 }
335309 if fields == nil {
336- return nil, errors.New("swarm protocol: payload is not an object")
310+ return nil, errors.New("subagent protocol: payload is not an object")
337311 }
338312 return fields, nil
339313 }
@@ -352,18 +326,18 @@ func (e Envelope) PayloadValue() (any, error) {
352326 // message id intact.
353327 func (e *Envelope) SetPayload(payload any) error {
354328 if e == nil {
355- return errors.New("swarm protocol: nil envelope")
329+ return errors.New("subagent protocol: nil envelope")
356330 }
357331 p, err := marshalPayload(payload)
358332 if err != nil {
359- return fmt.Errorf("swarm protocol payload: %w", err)
333+ return fmt.Errorf("subagent protocol payload: %w", err)
360334 }
361335 e.Payload = p
362336 return nil
363337 }
364338
365-// ParseEnvelope parses either a versioned envelope or a legacy flat event.
366-// Unknown event types and unknown payload fields are valid and retained.
339+// ParseEnvelope parses one current-protocol envelope. Unknown event types and
340+// unknown payload fields are valid and retained.
367341 func ParseEnvelope(data []byte) (Envelope, error) {
368342 data = bytes.TrimSpace(data)
369343 if len(data) == 0 {
@@ -371,19 +345,13 @@ func ParseEnvelope(data []byte) (Envelope, error) {
371345 }
372346 var raw map[string]json.RawMessage
373347 if err := json.Unmarshal(data, &raw); err != nil {
374- return Envelope{}, fmt.Errorf("swarm protocol: decode envelope: %w", err)
348+ return Envelope{}, fmt.Errorf("subagent protocol: decode envelope: %w", err)
375349 }
376350 if raw == nil {
377- return Envelope{}, errors.New("swarm protocol: envelope must be an object")
351+ return Envelope{}, errors.New("subagent protocol: envelope must be an object")
378352 }
379353 var e Envelope
380- if hasNumericVersion(raw) {
381- if err := unmarshalVersionedEnvelope(raw, &e); err != nil {
382- return Envelope{}, err
383- }
384- return e, nil
385- }
386- if err := unmarshalLegacyEvent(raw, &e); err != nil {
354+ if err := unmarshalVersionedEnvelope(raw, &e); err != nil {
387355 return Envelope{}, err
388356 }
389357 return e, nil
@@ -402,7 +370,7 @@ func ParseEnvelopeLine(line string) (Envelope, error) {
402370 return ParseEnvelope([]byte(line))
403371 }
404372
405-// ParseEvent parses a versioned or legacy JSON event. A known command is
373+// ParseEvent parses a current-protocol JSON event. A known command is
406374 // rejected; unknown types are accepted because an older peer cannot know
407375 // whether a future event name is present in its event registry.
408376 func ParseEvent(data []byte) (Envelope, error) {
@@ -422,55 +390,25 @@ func ParseProtocolEvent(data []byte) (Envelope, error) { return ParseEvent(data)
422390 // ParseEventLine parses one JSONL event line.
423391 func ParseEventLine(line string) (Envelope, error) { return ParseEvent([]byte(line)) }
424392
425-// ParseCommand parses either a versioned JSON command or one of the legacy
426-// text inbox commands: `user <prompt>`, `cancel`, and `shutdown`. The text
427-// form is normalised to a versioned envelope with a fresh message id.
393+// ParseCommand parses one current-protocol JSON command.
428394 func ParseCommand(line string) (Envelope, error) {
429- raw := strings.TrimRight(line, "\r\n")
430- if strings.TrimSpace(raw) == "" {
395+ raw := strings.TrimSpace(line)
396+ if raw == "" {
431397 return Envelope{}, ErrEmptyProtocolLine
432398 }
433- if strings.HasPrefix(strings.TrimSpace(raw), "{") {
434- e, err := ParseEnvelope([]byte(raw))
435- if err != nil {
436- return Envelope{}, err
437- }
438- if IsEventName(e.Type) {
439- return Envelope{}, ErrNotCommand
440- }
441- return e, nil
399+ e, err := ParseEnvelope([]byte(raw))
400+ if err != nil {
401+ return Envelope{}, err
402+ }
403+ if IsEventName(e.Type) {
404+ return Envelope{}, ErrNotCommand
442405 }
443- return ParseLegacyCommand(raw)
406+ return e, nil
444407 }
445408
446409 // ParseCommandLine is an alias for ParseCommand.
447410 func ParseCommandLine(line string) (Envelope, error) { return ParseCommand(line) }
448411
449-// ParseLegacyCommand converts the old text inbox command syntax to a current
450-// envelope. The old syntax had no agent or turn id, so those fields remain
451-// empty until the owning supervisor supplies them.
452-func ParseLegacyCommand(line string) (Envelope, error) {
453- if strings.TrimSpace(line) == "" {
454- return Envelope{}, ErrEmptyProtocolLine
455- }
456- var e Envelope
457- switch {
458- case strings.HasPrefix(line, "user "):
459- e = NewCommand(CommandTurnStart, "", "", TurnStartPayload{Prompt: strings.TrimPrefix(line, "user ")})
460- case strings.TrimSpace(line) == "cancel":
461- e = NewCommand(CommandTurnCancel, "", "", TurnCancelPayload{})
462- case strings.TrimSpace(line) == "shutdown":
463- e = NewCommand(CommandAgentShutdown, "", "", AgentShutdownPayload{})
464- case strings.TrimSpace(line) == "ping":
465- e = NewCommand(CommandAgentPing, "", "", AgentPingPayload{})
466- default:
467- return Envelope{}, fmt.Errorf("swarm protocol: unknown legacy command %q", line)
468- }
469- e.LegacyCommand = true
470- e.LegacyText = line
471- return e, nil
472-}
473-
474412 // MarshalEnvelope returns one JSON object without a trailing newline.
475413 func MarshalEnvelope(e Envelope) ([]byte, error) {
476414 return json.Marshal(e)
@@ -491,7 +429,7 @@ func EncodeEnvelope(e Envelope) ([]byte, error) { return MarshalJSONL(e) }
491429 // WriteEnvelope writes one complete newline-terminated JSONL message.
492430 func WriteEnvelope(w io.Writer, e Envelope) error {
493431 if w == nil {
494- return errors.New("swarm protocol: nil writer")
432+ return errors.New("subagent protocol: nil writer")
495433 }
496434 b, err := MarshalJSONL(e)
497435 if err != nil {
@@ -508,7 +446,7 @@ func WriteEnvelope(w io.Writer, e Envelope) error {
508446 // caller reading many messages should reuse the same bufio.Reader.
509447 func ReadEnvelope(r *bufio.Reader) (Envelope, error) {
510448 if r == nil {
511- return Envelope{}, errors.New("swarm protocol: nil reader")
449+ return Envelope{}, errors.New("subagent protocol: nil reader")
512450 }
513451 line, err := r.ReadBytes('\n')
514452 if len(bytes.TrimSpace(line)) == 0 && err == io.EOF {
@@ -524,86 +462,12 @@ func ReadEnvelope(r *bufio.Reader) (Envelope, error) {
524462 return Envelope{}, err
525463 }
526464
527-// MarshalLegacyCommand converts a normalised command back to the text inbox
528-// syntax. It is intended for compatibility bridges, not for new transports.
529-func MarshalLegacyCommand(e Envelope) (string, error) {
530- if !e.IsCommand() {
531- return "", ErrNotCommand
532- }
533- if e.LegacyCommand && e.LegacyText != "" {
534- return e.LegacyText, nil
535- }
536- fields, err := e.PayloadFields()
537- if err != nil {
538- return "", err
539- }
540- switch e.Type {
541- case CommandTurnStart:
542- var p TurnStartPayload
543- if err := e.DecodePayload(&p); err != nil {
544- return "", err
545- }
546- return "user " + p.Prompt, nil
547- case CommandTurnCancel:
548- return "cancel", nil
549- case CommandAgentShutdown:
550- return "shutdown", nil
551- case CommandAgentPing:
552- return "ping", nil
553- default:
554- _ = fields // retain the object check for useful errors above
555- return "", fmt.Errorf("swarm protocol: no legacy spelling for command %q", e.Type)
556- }
557-}
558-
559-// LegacyEvent converts an envelope to the package's existing flat Event
560-// representation. This is the compatibility bridge for the durable event
561-// reader while callers migrate to Envelope.
562-func (e Envelope) LegacyEvent() (Event, error) {
563- if e.IsCommand() {
564- return Event{}, ErrNotEvent
565- }
566- fields, err := e.PayloadFields()
567- if err != nil {
568- return Event{}, err
569- }
570- data := make(map[string]any, len(fields))
571- for key, raw := range fields {
572- var value any
573- if err := json.Unmarshal(raw, &value); err != nil {
574- return Event{}, fmt.Errorf("swarm protocol payload field %q: %w", key, err)
575- }
576- data[key] = value
577- }
578- return Event{
579- Time: e.Timestamp, Type: e.Type, Version: e.Version,
580- MessageID: e.MessageID, AgentID: e.AgentID, TurnID: e.TurnID,
581- Data: data,
582- }, nil
583-}
584-
585-// EnvelopeFromEvent upgrades an existing flat Event to the current protocol.
586-// The event's Data is copied into a raw payload and the generated envelope
587-// receives fresh metadata.
588-func EnvelopeFromEvent(ev Event, agentID, turnID string) Envelope {
589- return NewEventEnvelope(ev.Type, agentID, turnID, ev.Data)
590-}
591-
592-// EventFromEnvelope is an alias for LegacyEvent.
593-func EventFromEnvelope(e Envelope) (Event, error) { return e.LegacyEvent() }
594-
595465 func (e Envelope) MarshalJSON() ([]byte, error) {
596- if e.Legacy {
597- return e.marshalLegacyJSON()
598- }
599466 return e.marshalVersionedJSON()
600467 }
601468
602469 func (e Envelope) marshalVersionedJSON() ([]byte, error) {
603470 version := e.Version
604- if version == LegacyProtocolVersion {
605- version = ProtocolVersion
606- }
607471 messageID := e.MessageID
608472 if messageID == "" {
609473 messageID = NewMessageID()
@@ -617,7 +481,7 @@ func (e Envelope) marshalVersionedJSON() ([]byte, error) {
617481 payload = json.RawMessage("{}")
618482 }
619483 if !json.Valid(payload) {
620- return nil, errors.New("swarm protocol: invalid payload JSON")
484+ return nil, errors.New("subagent protocol: invalid payload JSON")
621485 }
622486 out := make(map[string]json.RawMessage, len(e.Unknown)+7)
623487 for key, value := range e.Unknown {
@@ -638,68 +502,22 @@ func (e Envelope) marshalVersionedJSON() ([]byte, error) {
638502 return json.Marshal(out)
639503 }
640504
641-func (e Envelope) marshalLegacyJSON() ([]byte, error) {
642- fields, err := e.PayloadFields()
643- if err != nil {
644- return nil, err
645- }
646- out := make(map[string]json.RawMessage, len(fields)+5)
647- for key, value := range fields {
648- out[key] = cloneRaw(value)
649- }
650- for key, value := range e.Unknown {
651- if isLegacyEnvelopeField(key) {
652- continue
653- }
654- if _, exists := out[key]; !exists {
655- out[key] = cloneRaw(value)
656- }
657- }
658- putRaw(out, "type", e.Type)
659- if !e.Timestamp.IsZero() {
660- putRaw(out, "time", e.Timestamp)
661- }
662- if e.MessageID != "" {
663- putRaw(out, "message_id", e.MessageID)
664- }
665- if e.AgentID != "" {
666- putRaw(out, "agent_id", e.AgentID)
667- }
668- if e.TurnID != "" {
669- putRaw(out, "turn_id", e.TurnID)
670- }
671- return json.Marshal(out)
672-}
673-
674505 func (e *Envelope) UnmarshalJSON(data []byte) error {
675506 var raw map[string]json.RawMessage
676507 if err := json.Unmarshal(data, &raw); err != nil {
677508 return err
678509 }
679510 if raw == nil {
680- return errors.New("swarm protocol: envelope must be an object")
511+ return errors.New("subagent protocol: envelope must be an object")
681512 }
682513 var parsed Envelope
683- if hasNumericVersion(raw) {
684- if err := unmarshalVersionedEnvelope(raw, &parsed); err != nil {
685- return err
686- }
687- } else if err := unmarshalLegacyEvent(raw, &parsed); err != nil {
514+ if err := unmarshalVersionedEnvelope(raw, &parsed); err != nil {
688515 return err
689516 }
690517 *e = parsed
691518 return nil
692519 }
693520
694-func hasNumericVersion(raw map[string]json.RawMessage) bool {
695- value, ok := raw["version"]
696- if !ok {
697- return false
698- }
699- var version int
700- return json.Unmarshal(value, &version) == nil
701-}
702-
703521 func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) error {
704522 if err := decodeRequired(raw, "version", &e.Version); err != nil {
705523 return err
@@ -721,7 +539,7 @@ func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) err
721539 }
722540 if timestamp, ok := raw["timestamp"]; ok {
723541 if err := json.Unmarshal(timestamp, &e.Timestamp); err != nil {
724- return fmt.Errorf("swarm protocol: timestamp: %w", err)
542+ return fmt.Errorf("subagent protocol: timestamp: %w", err)
725543 }
726544 }
727545 if payload, ok := raw["payload"]; ok {
@@ -736,57 +554,17 @@ func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) err
736554 return nil
737555 }
738556
739-func unmarshalLegacyEvent(raw map[string]json.RawMessage, e *Envelope) error {
740- if err := decodeRequired(raw, "type", &e.Type); err != nil {
741- return err
742- }
743- e.Version = LegacyProtocolVersion
744- e.Legacy = true
745- if err := decodeOptionalString(raw, "message_id", &e.MessageID); err != nil {
746- return err
747- }
748- if err := decodeOptionalString(raw, "agent_id", &e.AgentID); err != nil {
749- return err
750- }
751- if err := decodeOptionalString(raw, "turn_id", &e.TurnID); err != nil {
752- return err
753- }
754- if value, ok := raw["time"]; ok {
755- // Legacy Event historically ignored an invalid timestamp. Keep that
756- // permissive behaviour while still retaining all other fields.
757- _ = json.Unmarshal(value, &e.Timestamp)
758- }
759- payload := make(map[string]json.RawMessage)
760- for key, value := range raw {
761- switch key {
762- case "type", "time", "message_id", "agent_id", "turn_id":
763- continue
764- }
765- payload[key] = cloneRaw(value)
766- }
767- if len(payload) == 0 {
768- e.Payload = json.RawMessage("{}")
769- } else {
770- encoded, err := json.Marshal(payload)
771- if err != nil {
772- return err
773- }
774- e.Payload = encoded
775- }
776- return nil
777-}
778-
779557 func decodeRequired(raw map[string]json.RawMessage, name string, dst any) error {
780558 value, ok := raw[name]
781559 if !ok {
782- return fmt.Errorf("swarm protocol: missing %s", name)
560+ return fmt.Errorf("subagent protocol: missing %s", name)
783561 }
784562 if err := json.Unmarshal(value, dst); err != nil {
785- return fmt.Errorf("swarm protocol: %s: %w", name, err)
563+ return fmt.Errorf("subagent protocol: %s: %w", name, err)
786564 }
787565 if name == "type" {
788566 if text, ok := dst.(*string); ok && strings.TrimSpace(*text) == "" {
789- return errors.New("swarm protocol: empty message type")
567+ return errors.New("subagent protocol: empty message type")
790568 }
791569 }
792570 return nil
@@ -798,7 +576,7 @@ func decodeOptionalString(raw map[string]json.RawMessage, name string, dst *stri
798576 return nil
799577 }
800578 if err := json.Unmarshal(value, dst); err != nil {
801- return fmt.Errorf("swarm protocol: %s: %w", name, err)
579+ return fmt.Errorf("subagent protocol: %s: %w", name, err)
802580 }
803581 return nil
804582 }
@@ -826,15 +604,6 @@ func isEnvelopeField(key string) bool {
826604 }
827605 }
828606
829-func isLegacyEnvelopeField(key string) bool {
830- switch key {
831- case "type", "time", "message_id", "agent_id", "turn_id", "version", "timestamp", "payload":
832- return true
833- default:
834- return false
835- }
836-}
837-
838607 func putRaw(out map[string]json.RawMessage, key string, value any) {
839608 b, err := json.Marshal(value)
840609 if err == nil {
@@ -852,8 +621,8 @@ func cloneRaw(value json.RawMessage) json.RawMessage {
852621 type AgentPingPayload struct{}
853622 type AgentShutdownPayload struct{}
854623 type AgentReadyPayload struct {
855- // Version is the worker/application version used by the legacy ready
856- // event. WorkerVersion is the unambiguous spelling for new producers.
624+ // Version is the worker/application version. WorkerVersion is the
625+ // unambiguous spelling for new producers.
857626 Version string `json:"version,omitempty"`
858627 WorkerVersion string `json:"worker_version,omitempty"`
859628 CWD string `json:"cwd,omitempty"`
R packages/agent/swarm/protocol_test.go → packages/agent/subagents/protocol_test.go +2 -80 renamed, 76% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "bufio"
@@ -102,84 +102,6 @@ func TestProtocolPreservesUnknownEventAndPayload(t *testing.T) {
102102 }
103103 }
104104
105-func TestProtocolReadsAndRetainsLegacyFlatEvent(t *testing.T) {
106- // Legacy agent.ready events historically used a string `version`
107- // payload field, so its presence must not be mistaken for the numeric
108- // protocol version field.
109- input := `{"type":"assistant_message","time":"2026-08-04T12:00:00.123Z","version":"zot-test","content":[{"type":"text","text":"hello"}],"new_field":{"v":7}}`
110- e, err := ParseEvent([]byte(input))
111- if err != nil {
112- t.Fatalf("ParseEvent: %v", err)
113- }
114- if !e.Legacy || e.Version != LegacyProtocolVersion {
115- t.Fatalf("legacy marker/version = %v/%d", e.Legacy, e.Version)
116- }
117- if e.Type != "assistant_message" || e.Timestamp.IsZero() {
118- t.Fatalf("legacy metadata = %+v", e)
119- }
120- legacy, err := e.LegacyEvent()
121- if err != nil {
122- t.Fatalf("LegacyEvent: %v", err)
123- }
124- if legacy.Type != e.Type || legacy.Data["version"] != "zot-test" || legacy.Data["new_field"] == nil {
125- t.Fatalf("legacy conversion lost data: %+v", legacy)
126- }
127-
128- encoded, err := MarshalEnvelope(e)
129- if err != nil {
130- t.Fatalf("MarshalEnvelope legacy: %v", err)
131- }
132- var got map[string]any
133- if err := json.Unmarshal(encoded, &got); err != nil {
134- t.Fatalf("legacy output JSON: %v", err)
135- }
136- if got["version"] != "zot-test" {
137- t.Fatalf("legacy payload version was changed: %#v", got["version"])
138- }
139- if got["type"] != "assistant_message" || got["new_field"] == nil {
140- t.Fatalf("legacy fields not retained: %#v", got)
141- }
142-}
143-
144-func TestProtocolParsesLegacyTextCommands(t *testing.T) {
145- tests := []struct {
146- line string
147- kind string
148- want string
149- }{
150- {"user inspect the parser ", CommandTurnStart, "inspect the parser "},
151- {"cancel", CommandTurnCancel, ""},
152- {"shutdown", CommandAgentShutdown, ""},
153- }
154- for _, tt := range tests {
155- t.Run(tt.line, func(t *testing.T) {
156- e, err := ParseCommand(tt.line)
157- if err != nil {
158- t.Fatalf("ParseCommand: %v", err)
159- }
160- if e.Type != tt.kind || e.Version != ProtocolVersion || e.MessageID == "" {
161- t.Fatalf("command metadata = %+v", e)
162- }
163- if !e.LegacyCommand || e.LegacyText != tt.line {
164- t.Fatalf("legacy source was not retained: %+v", e)
165- }
166- if tt.want != "" {
167- var payload TurnStartPayload
168- if err := e.DecodePayload(&payload); err != nil {
169- t.Fatalf("DecodePayload: %v", err)
170- }
171- if payload.Prompt != tt.want {
172- t.Fatalf("prompt = %q, want %q", payload.Prompt, tt.want)
173- }
174- }
175- legacy, err := MarshalLegacyCommand(e)
176- if err != nil || legacy != tt.line {
177- t.Fatalf("MarshalLegacyCommand = %q, %v; want %q", legacy, err, tt.line)
178- }
179- })
180- }
181-}
182-
183105 func TestProtocolJSONCommandsAndDirectionChecks(t *testing.T) {
184106 command := NewCommand(CommandTurnStart, "agent-1", "turn-2", TurnStartPayload{Prompt: "hello"})
185107 line, err := MarshalJSONL(command)
@@ -266,7 +188,7 @@ func TestProtocolUnsupportedVersion(t *testing.T) {
266188 }
267189 }
268190
269-func TestProtocolLegacyEmptyAndMalformedInputs(t *testing.T) {
191+func TestProtocolEmptyAndMalformedInputs(t *testing.T) {
270192 for _, input := range []string{"", " ", "not a command"} {
271193 if _, err := ParseCommand(input); err == nil {
272194 t.Errorf("ParseCommand(%q) succeeded", input)
R packages/agent/swarm/result.go → packages/agent/subagents/result.go +7 -7 renamed, 96% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -248,7 +248,7 @@ func writeTurnResult(stateDir string, result *TurnResult) error {
248248 return nil
249249 }
250250
251-func (f *Swarm) captureWorkspace(a *Agent) {
251+func (f *Supervisor) captureWorkspace(a *Agent) {
252252 if a == nil || a.workspaceCapture == nil {
253253 return
254254 }
@@ -280,7 +280,7 @@ func (f *Swarm) captureWorkspace(a *Agent) {
280280 a.lifecycleMu.Unlock()
281281 }
282282
283-func (f *Swarm) ensureResult(a *Agent, status Status, runErr error) {
283+func (f *Supervisor) ensureResult(a *Agent, status Status, runErr error) {
284284 if a == nil {
285285 return
286286 }
@@ -371,7 +371,7 @@ func (f *Swarm) ensureResult(a *Agent, status Status, runErr error) {
371371 }
372372
373373 // ReadResult returns the durable structured result for an agent.
374-func (f *Swarm) ReadResult(id string) (*TurnResult, error) {
374+func (f *Supervisor) ReadResult(id string) (*TurnResult, error) {
375375 a := f.Get(id)
376376 if a == nil {
377377 return nil, fmt.Errorf("subagents: no such agent %q", id)
@@ -392,9 +392,9 @@ func (f *Swarm) ReadResult(id string) (*TurnResult, error) {
392392 return result.Bounded(f.cfg.Policy.MaxOutputBytes, f.cfg.Policy.MaxOutputLines), nil
393393 }
394394
395-func (f *Swarm) ResultReference(id string) string { return ResultRef(id) }
396-func (f *Swarm) HistoryReference(id string) string { return HistoryRef(id) }
397-func (f *Swarm) PatchReference(id string) string { return PatchRef(id) }
395+func (f *Supervisor) ResultReference(id string) string { return ResultRef(id) }
396+func (f *Supervisor) HistoryReference(id string) string { return HistoryRef(id) }
397+func (f *Supervisor) PatchReference(id string) string { return PatchRef(id) }
398398
399399 func readTurnResult(stateDir string) (*TurnResult, error) {
400400 file, err := os.Open(resultPath(stateDir))
R packages/agent/swarm/result_test.go → packages/agent/subagents/result_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "encoding/json"
R packages/agent/swarm/runner.go → packages/agent/subagents/runner.go +48 -53 renamed, 88% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "bufio"
@@ -21,7 +21,7 @@ import (
2121 //
2222 // Why a long-lived daemon and not `zot --print`: the supervisor and
2323 // the user expect agents to keep accepting follow-up prompts. A
24-// one-shot subprocess can't do that; this design gives each swarm
24+// one-shot subprocess can't do that; this design gives each subagent
2525 // agent a persistent session file plus an inbox socket the parent
2626 // writes to, mirroring Claude Code's "Agents view" model.
2727 //
@@ -45,8 +45,8 @@ type execRunner struct {
4545 Command []string
4646
4747 // SessionPath is the agent's session file. Empty means "defer
48- // to r.agent.SessionPath", which Swarm.Spawn always populates
49- // with <swarm-root>/agents/<id>/session.json. Tests that
48+ // to r.agent.SessionPath", which Supervisor.Spawn always populates
49+ // with <subagent-root>/agents/<id>/session.json. Tests that
5050 // hand-build an Agent without going through Spawn must set
5151 // one of the two; the runner refuses to invent a fallback
5252 // because the only plausible one (<Dir>/.zot/session.json)
@@ -55,11 +55,11 @@ type execRunner struct {
5555 SessionPath string
5656 }
5757
58-// swarmAgentArgsOpts captures every dynamic input to swarmAgentArgs.
58+// subagentWorkerArgsOpts captures every dynamic input to subagentWorkerArgs.
5959 // The fields map 1:1 onto child CLI flags; empty values omit the flag
6060 // entirely and let the child resolve a default the same way a normal
6161 // `zot` invocation does.
62-type swarmAgentArgsOpts struct {
62+type subagentWorkerArgsOpts struct {
6363 Exe string
6464 Dir string
6565 SessionPath string
@@ -96,7 +96,7 @@ func defaultChildArgs(exe string, a *Agent, sessionPath, inboxPath string) []str
9696 if a.Resuming {
9797 task = ""
9898 }
99- return swarmAgentArgs(swarmAgentArgsOpts{
99+ return subagentWorkerArgs(subagentWorkerArgsOpts{
100100 Exe: exe,
101101 Dir: a.Dir,
102102 SessionPath: sessionPath,
@@ -115,16 +115,13 @@ func defaultChildArgs(exe string, a *Agent, sessionPath, inboxPath string) []str
115115 })
116116 }
117117
118-// swarmAgentArgs builds the argv used when execRunner.Command is
118+// subagentWorkerArgs builds the argv used when execRunner.Command is
119119 // empty. Pulled out so tests can lock in the flag set without
120120 // actually spawning a subprocess.
121-func swarmAgentArgs(opts swarmAgentArgsOpts) []string {
121+func subagentWorkerArgs(opts subagentWorkerArgsOpts) []string {
122122 args := []string{
123123 opts.Exe,
124124 "--subagent-worker", opts.InboxPath,
125- // Keep the old flag during the compatibility window. The child
126- // parser accepts both and canonicalises the mode internally.
127- "--swarm-agent", opts.InboxPath,
128125 "--session", opts.SessionPath,
129126 "--cwd", opts.Dir,
130127 }
@@ -171,9 +168,9 @@ func swarmAgentArgs(opts swarmAgentArgsOpts) []string {
171168 func (r *execRunner) Run(ctx context.Context, sink Sink) error {
172169 // SessionPath resolution order:
173170 // 1. explicit r.SessionPath set by the test / caller
174- // 2. r.agent.SessionPath baked in by Swarm.Spawn — the
171+ // 2. r.agent.SessionPath baked in by Supervisor.Spawn — the
175172 // production path. Always lives under
176- // <swarm-root>/agents/<id>/session.json so the per-
173+ // <subagent-root>/agents/<id>/session.json so the per-
177174 // agent state is entirely outside the working tree.
178175 // Crucial because Agent.Dir points at the user's repo;
179176 // any .zot/ scratch directory under Dir would litter
@@ -189,7 +186,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
189186 sessionPath = r.agent.SessionPath
190187 }
191188 if sessionPath == "" {
192- return fmt.Errorf("swarm: agent missing session path (set SpawnRequest via Swarm.SpawnReq, or hand-build Agent with SessionPath populated)")
189+ return fmt.Errorf("subagent: agent missing session path (set SpawnRequest via Supervisor.SpawnReq, or hand-build Agent with SessionPath populated)")
193190 }
194191 if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil {
195192 return fmt.Errorf("session dir: %w", err)
@@ -198,7 +195,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
198195 inboxPath := r.agent.InboxPath
199196 logPath := r.agent.EventLogPath
200197 if logPath == "" {
201- return fmt.Errorf("swarm: agent missing event log path")
198+ return fmt.Errorf("subagent: agent missing event log path")
202199 }
203200 log, err := OpenEventLog(logPath)
204201 if err != nil {
@@ -219,30 +216,30 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
219216 configureWorkerProcess(cmd)
220217 cmd.Dir = r.agent.Dir
221218 cmd.Env = append(workerEnvironment(r.agent.Provider),
222- "ZOT_SWARM_AGENT_ID="+r.agent.ID,
223- "ZOT_SWARM_EVENT_LOG="+logPath,
219+ "ZOT_SUBAGENT_AGENT_ID="+r.agent.ID,
220+ "ZOT_SUBAGENT_EVENT_LOG="+logPath,
224221 )
225222 if r.agent.HeartbeatInterval > 0 {
226- cmd.Env = append(cmd.Env, "ZOT_SWARM_HEARTBEAT_INTERVAL="+r.agent.HeartbeatInterval.String())
223+ cmd.Env = append(cmd.Env, "ZOT_SUBAGENT_HEARTBEAT_INTERVAL="+r.agent.HeartbeatInterval.String())
227224 }
228225 if r.agent.maxOutputBytes > 0 {
229- cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SWARM_MAX_OUTPUT_BYTES=%d", r.agent.maxOutputBytes))
226+ cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SUBAGENT_MAX_OUTPUT_BYTES=%d", r.agent.maxOutputBytes))
230227 }
231228 if r.agent.maxOutputLines > 0 {
232- cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SWARM_MAX_OUTPUT_LINES=%d", r.agent.maxOutputLines))
229+ cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SUBAGENT_MAX_OUTPUT_LINES=%d", r.agent.maxOutputLines))
233230 }
234231 if r.resolveCredential != nil {
235232 credential, resolveErr := r.resolveCredential(ctx, r.agent.Provider)
236233 if resolveErr != nil {
237- return fmt.Errorf("resolve swarm credential for %s: %w", r.agent.Provider, resolveErr)
234+ return fmt.Errorf("resolve subagent credential for %s: %w", r.agent.Provider, resolveErr)
238235 }
239236 if credential.Value != "" {
240237 encoded, encodeErr := json.Marshal(credential)
241238 if encodeErr != nil {
242- return fmt.Errorf("encode swarm credential: %w", encodeErr)
239+ return fmt.Errorf("encode subagent credential: %w", encodeErr)
243240 }
244241 cmd.Stdin = bytes.NewReader(encoded)
245- cmd.Env = append(cmd.Env, "ZOT_SWARM_CREDENTIAL_STDIN=1")
242+ cmd.Env = append(cmd.Env, "ZOT_SUBAGENT_CREDENTIAL_STDIN=1")
246243 }
247244 }
248245
@@ -317,7 +314,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
317314 // also forwards provider/tool-loop turn_end
318315 // events (for example stop=tool_use); those do
319316 // not contain step and must not be treated as
320- // swarm task completion.
317+ // subagent task completion.
321318 notifyPromptTurnEnd(r.agent, ev)
322319 } else {
323320 sink.Transcript(trimmed)
@@ -376,20 +373,20 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
376373 if err != nil && ctx.Err() != nil {
377374 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "cancelled"}))
378375 if logErr := firstLogErr(); logErr != nil {
379- return fmt.Errorf("swarm event log: %w", logErr)
376+ return fmt.Errorf("subagent event log: %w", logErr)
380377 }
381378 return ctx.Err()
382379 }
383380 if err != nil {
384381 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": exit, "error": err.Error()}))
385382 if logErr := firstLogErr(); logErr != nil {
386- return fmt.Errorf("swarm event log: %v; worker: %w", logErr, err)
383+ return fmt.Errorf("subagent event log: %v; worker: %w", logErr, err)
387384 }
388385 return err
389386 }
390387 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": 0}))
391388 if logErr := firstLogErr(); logErr != nil {
392- return fmt.Errorf("swarm event log: %w", logErr)
389+ return fmt.Errorf("subagent event log: %w", logErr)
393390 }
394391 sink.Activity("done")
395392 return nil
@@ -475,35 +472,36 @@ func readBoundedLineStats(r *bufio.Reader, limit int) ([]byte, error, bool, int6
475472 }
476473 }
477474
478-// parseEventLine attempts to decode one JSONL line as an Event.
479-// Returns ok=false for non-JSON or JSON without a "type" field.
475+// parseEventLine attempts to decode one current-protocol JSONL line as an Event.
480476 func parseEventLine(line string) (Event, bool) {
481477 if len(line) == 0 || line[0] != '{' {
482478 return Event{}, false
483479 }
484- if env, err := ParseEvent([]byte(line)); err == nil && !env.Legacy {
485- if ev, convertErr := env.LegacyEvent(); convertErr == nil {
486- ev.Version = env.Version
487- ev.MessageID = env.MessageID
488- ev.AgentID = env.AgentID
489- ev.TurnID = env.TurnID
490- if ev.Time.IsZero() {
491- ev.Time = time.Now()
492- }
493- return ev, true
494- }
495- }
496- var ev Event
497- if err := json.Unmarshal([]byte(line), &ev); err != nil {
480+ env, err := ParseEvent([]byte(line))
481+ if err != nil {
498482 return Event{}, false
499483 }
500- if ev.Type == "" {
484+ fields, err := env.PayloadFields()
485+ if err != nil {
501486 return Event{}, false
502487 }
503- if ev.Time.IsZero() {
504- ev.Time = time.Now()
488+ data := make(map[string]any, len(fields))
489+ for key, raw := range fields {
490+ var value any
491+ if err := json.Unmarshal(raw, &value); err != nil {
492+ return Event{}, false
493+ }
494+ data[key] = value
495+ }
496+ when := env.Timestamp
497+ if when.IsZero() {
498+ when = time.Now()
505499 }
506- return ev, true
500+ return Event{
501+ Time: when, Type: env.Type, Version: env.Version,
502+ MessageID: env.MessageID, AgentID: env.AgentID, TurnID: env.TurnID,
503+ Data: data,
504+ }, true
507505 }
508506
509507 func updateAgentFromEvent(a *Agent, ev Event) {
@@ -514,9 +512,6 @@ func updateAgentFromEvent(a *Agent, ev Event) {
514512 if now.IsZero() {
515513 now = time.Now()
516514 }
517- if ev.Version > 0 {
518- a.setProtocolVersion(ev.Version)
519- }
520515 a.markActivity(now)
521516 persist := false
522517 switch ev.Type {
@@ -585,7 +580,7 @@ func updateAgentFromEvent(a *Agent, ev Event) {
585580 }
586581 }
587582
588-// notifyPromptTurnEnd calls Agent.OnTurnEnd only for the swarm
583+// notifyPromptTurnEnd calls Agent.OnTurnEnd only for the subagent
589584 // daemon's prompt-level completion event. Provider/tool-loop
590585 // turn_end events (such as stop=tool_use) do not include step and
591586 // are not terminal for the delegated task.
@@ -667,7 +662,7 @@ func applyEventToSink(ev Event, sink Sink) {
667662 case "agent_ready", EventAgentReady, EventAgentHeartbeat:
668663 sink.Activity("idle")
669664 case "agent_stopped":
670- // terminal status is decided by Swarm.run from the runner's
665+ // terminal status is decided by Supervisor.run from the runner's
671666 // return value, not from this event. Don't overwrite the
672667 // activity here.
673668 case "error":
R packages/agent/swarm/runner_e2e_test.go → packages/agent/subagents/runner_e2e_test.go +9 -9 renamed, 88% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -15,11 +15,11 @@ import (
1515 // TestRunnerEndToEndWithStubChild is the integration test for the
1616 // new daemon-mode runner. It compiles the stubchild binary in
1717 // testdata/cmd/stubchild, points an execRunner at it, and drives
18-// a Swarm through one Spawn + one SendUserTurn + Stop cycle.
18+// a Supervisor through one Spawn + one SendUserTurn + Stop cycle.
1919 //
2020 // What this proves:
2121 //
22-// - The default argv shape (swarmAgentArgs) is one the child can
22+// - The default argv shape (subagentWorkerArgs) is one the child can
2323 // actually parse — locks the shape against silent breakage.
2424 // - The stdout JSONL parser ingests events and writes them to the
2525 // durable log.
@@ -50,7 +50,7 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) {
5050 NewRunner: func(a *Agent) Runner {
5151 return &execRunner{
5252 agent: a,
53- Command: swarmAgentArgs(swarmAgentArgsOpts{
53+ Command: subagentWorkerArgs(subagentWorkerArgsOpts{
5454 Exe: exe,
5555 Dir: a.Dir,
5656 SessionPath: a.SessionPath,
@@ -94,13 +94,13 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) {
9494
9595 // Send a follow-up over the inbox. The stub echoes the text into
9696 // an assistant_message we can poll for in the log.
97- if err := retrySend(f, a.ID, "user follow up", time.Second); err != nil {
98- t.Fatalf("SendInput: %v", err)
97+ if err := retrySend(f, a.ID, "follow up", time.Second); err != nil {
98+ t.Fatalf("SendUserTurn: %v", err)
9999 }
100100 waitFor("echo: follow up")
101101
102102 // Shut the agent down gracefully via the inbox.
103- if err := f.SendInput(a.ID, "shutdown"); err != nil && !errors.Is(err, ErrNotReady) {
103+ if err := a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})); err != nil && !errors.Is(err, ErrNotReady) {
104104 t.Fatalf("shutdown send: %v", err)
105105 }
106106 a.Wait()
@@ -112,11 +112,11 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) {
112112 // retrySend exists because the inbox dial races against the child
113113 // opening the socket. Production callers handle ErrNotReady with a
114114 // status message; tests retry within a small window.
115-func retrySend(f *Swarm, id, msg string, timeout time.Duration) error {
115+func retrySend(f *Supervisor, id, msg string, timeout time.Duration) error {
116116 deadline := time.Now().Add(timeout)
117117 var lastErr error
118118 for time.Now().Before(deadline) {
119- err := f.SendInput(id, msg)
119+ err := f.SendUserTurn(id, msg)
120120 if err == nil {
121121 return nil
122122 }
R packages/agent/swarm/runner_test.go → packages/agent/subagents/runner_test.go +20 -20 renamed, 90% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -11,8 +11,8 @@ import (
1111 "time"
1212 )
1313
14-// TestSwarmAgentArgs locks in the exact flag set the subprocess
15-// runner uses to start a swarm agent in daemon mode. Past
14+// TestSubagentWorkerArgs locks in the exact flag set the subprocess
15+// runner uses to start a subagent agent in daemon mode. Past
1616 // regressions in this area:
1717 //
1818 // - "--no-sess" instead of "--no-session" (old print-mode runner):
@@ -31,10 +31,10 @@ import (
3131 // positions. If a flag is renamed, update both the runner and this
3232 // test so we notice immediately.
3333 func TestCredentialStdinHelperProcess(t *testing.T) {
34- if os.Getenv("ZOT_SWARM_CREDENTIAL_HELPER") != "1" {
34+ if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_HELPER") != "1" {
3535 return
3636 }
37- if os.Getenv("ZOT_SWARM_CREDENTIAL_STDIN") != "1" {
37+ if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_STDIN") != "1" {
3838 os.Exit(2)
3939 }
4040 var credential Credential
@@ -65,7 +65,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) {
6565 t.Setenv("GITHUB_TOKEN", "github-secret")
6666 t.Setenv("CUSTOM_PROVIDER_TOKEN", "token-secret")
6767 t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/tmp/service-account.json")
68- t.Setenv("ZOT_SWARM_TEST_VALUE", "not-secret")
68+ t.Setenv("ZOT_SUBAGENT_TEST_VALUE", "not-secret")
6969 env := strings.Join(workerEnvironment("openai"), "\n")
7070 for _, secret := range []string{
7171 "OPENAI_API_KEY=openai-secret",
@@ -79,7 +79,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) {
7979 t.Fatalf("worker environment leaked %s", secret)
8080 }
8181 }
82- if !strings.Contains(env, "ZOT_SWARM_TEST_VALUE=not-secret") {
82+ if !strings.Contains(env, "ZOT_SUBAGENT_TEST_VALUE=not-secret") {
8383 t.Fatal("worker environment dropped unrelated variables")
8484 }
8585
@@ -91,7 +91,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) {
9191 }
9292
9393 func TestExecRunnerTransfersCredentialOnlyOnStdin(t *testing.T) {
94- t.Setenv("ZOT_SWARM_CREDENTIAL_HELPER", "1")
94+ t.Setenv("ZOT_SUBAGENT_CREDENTIAL_HELPER", "1")
9595 root := t.TempDir()
9696 a := &Agent{
9797 ID: "credential-test",
@@ -138,8 +138,8 @@ func TestApplyEventPreservesMultilineRolesAndFinalAssistant(t *testing.T) {
138138 }
139139 }
140140
141-func TestSwarmAgentArgs(t *testing.T) {
142- args := swarmAgentArgs(swarmAgentArgsOpts{
141+func TestSubagentWorkerArgs(t *testing.T) {
142+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
143143 Exe: "/path/to/zot",
144144 Dir: "/tmp/worktree",
145145 SessionPath: "/tmp/state/session.json",
@@ -161,11 +161,11 @@ func TestSwarmAgentArgs(t *testing.T) {
161161 }
162162
163163 mustHave := map[string]string{
164- "--swarm-agent": "/tmp/state/in.sock",
165- "--session": "/tmp/state/session.json",
166- "--cwd": "/tmp/worktree",
167- "--reasoning": "high",
168- "--subagent": "reviewer",
164+ "--subagent-worker": "/tmp/state/in.sock",
165+ "--session": "/tmp/state/session.json",
166+ "--cwd": "/tmp/worktree",
167+ "--reasoning": "high",
168+ "--subagent": "reviewer",
169169 }
170170 for flag, value := range mustHave {
171171 i := indexOf(args, flag)
@@ -187,8 +187,8 @@ func TestSwarmAgentArgs(t *testing.T) {
187187 }
188188 }
189189
190-func TestSwarmAgentArgsPropagatesProviderConnectionSettings(t *testing.T) {
191- args := swarmAgentArgs(swarmAgentArgsOpts{
190+func TestSubagentWorkerArgsPropagatesProviderConnectionSettings(t *testing.T) {
191+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
192192 Exe: "/zot",
193193 Dir: "/work",
194194 SessionPath: "/state/session.json",
@@ -224,11 +224,11 @@ func TestDefaultChildArgsPropagatesProviderConnectionSettings(t *testing.T) {
224224 }
225225 }
226226
227-// TestSwarmAgentArgsEmptyTaskOmitsPositional makes sure that when the
227+// TestSubagentWorkerArgsEmptyTaskOmitsPositional makes sure that when the
228228 // agent is being adopted (no fresh task) we don't pass an empty
229229 // positional which the arg parser would treat as a real prompt.
230-func TestSwarmAgentArgsEmptyTaskOmitsPositional(t *testing.T) {
231- args := swarmAgentArgs(swarmAgentArgsOpts{
230+func TestSubagentWorkerArgsEmptyTaskOmitsPositional(t *testing.T) {
231+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
232232 Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock",
233233 })
234234 for _, a := range args {
R packages/agent/swarm/scheduler.go → packages/agent/subagents/scheduler.go +14 -14 renamed, 90% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -10,7 +10,7 @@ import (
1010 "time"
1111 )
1212
13-func (f *Swarm) validateSpawnRequest(req SpawnRequest) error {
13+func (f *Supervisor) validateSpawnRequest(req SpawnRequest) error {
1414 if err := f.validateSpawnOptions(req); err != nil {
1515 return err
1616 }
@@ -32,7 +32,7 @@ func (f *Swarm) validateSpawnRequest(req SpawnRequest) error {
3232 return validateWorkspaceRoot(root, mode, allowedRoots)
3333 }
3434
35-func (f *Swarm) validateSpawnOptions(req SpawnRequest) error {
35+func (f *Supervisor) validateSpawnOptions(req SpawnRequest) error {
3636 p := f.cfg.Policy
3737 if req.MaxTurns < 0 || req.MaxTurns > p.MaxTurns {
3838 return fmt.Errorf("subagents: max_turns must be between 1 and %d", p.MaxTurns)
@@ -112,7 +112,7 @@ func containmentPath(path string) (string, error) {
112112 return filepath.Clean(absolute), nil
113113 }
114114
115-func (f *Swarm) schedule() {
115+func (f *Supervisor) schedule() {
116116 var starts []*Agent
117117 f.mu.Lock()
118118 for len(f.queue) > 0 && f.active < f.cfg.Policy.MaxConcurrent {
@@ -152,7 +152,7 @@ func (f *Swarm) schedule() {
152152 }
153153 }
154154
155-func (f *Swarm) startLifecycleMonitor(a *Agent) {
155+func (f *Supervisor) startLifecycleMonitor(a *Agent) {
156156 if a == nil || f.cfg.Policy.IdleTimeout <= 0 {
157157 return
158158 }
@@ -173,7 +173,7 @@ func (f *Swarm) startLifecycleMonitor(a *Agent) {
173173 continue
174174 }
175175 last := a.LastActivity()
176- if a.protocolReady() && !last.IsZero() && heartbeatGrace > 0 && time.Since(last) >= heartbeatGrace {
176+ if !last.IsZero() && heartbeatGrace > 0 && time.Since(last) >= heartbeatGrace {
177177 if err := f.Ping(a.ID); err != nil {
178178 a.setProcessState(ProcessDetached)
179179 a.setActivity("detached")
@@ -192,7 +192,7 @@ func (f *Swarm) startLifecycleMonitor(a *Agent) {
192192
193193 // Ping asks a live worker for a heartbeat. A detached or terminal worker
194194 // returns the same not-ready error as other inbox operations.
195-func (f *Swarm) Ping(id string) error {
195+func (f *Supervisor) Ping(id string) error {
196196 a := f.Get(id)
197197 if a == nil {
198198 return fmt.Errorf("subagents: no such agent %q", id)
@@ -203,7 +203,7 @@ func (f *Swarm) Ping(id string) error {
203203 return a.inbox.SendCommand(NewCommand(CommandAgentPing, a.ID, a.CurrentTurnID(), AgentPingPayload{}))
204204 }
205205
206-func (f *Swarm) nextRunnableIndexLocked() int {
206+func (f *Supervisor) nextRunnableIndexLocked() int {
207207 if f.active >= f.cfg.Policy.MaxConcurrent {
208208 return -1
209209 }
@@ -237,7 +237,7 @@ func parentKey(a *Agent) string {
237237 return a.ParentID
238238 }
239239
240-func (f *Swarm) armQueueTimeout(a *Agent) {
240+func (f *Supervisor) armQueueTimeout(a *Agent) {
241241 if a == nil {
242242 return
243243 }
@@ -250,9 +250,9 @@ func (f *Swarm) armQueueTimeout(a *Agent) {
250250 }()
251251 }
252252
253-func (f *Swarm) expireQueued(id string) { f.cancelQueued(id, context.DeadlineExceeded) }
253+func (f *Supervisor) expireQueued(id string) { f.cancelQueued(id, context.DeadlineExceeded) }
254254
255-func (f *Swarm) removeQueued(a *Agent) {
255+func (f *Supervisor) removeQueued(a *Agent) {
256256 if a == nil {
257257 return
258258 }
@@ -266,7 +266,7 @@ func (f *Swarm) removeQueued(a *Agent) {
266266 f.mu.Unlock()
267267 }
268268
269-func (f *Swarm) cancelQueued(id string, cause error) {
269+func (f *Supervisor) cancelQueued(id string, cause error) {
270270 a := f.Get(id)
271271 if a == nil {
272272 return
@@ -320,7 +320,7 @@ func (f *Swarm) cancelQueued(id string, cause error) {
320320 f.schedule()
321321 }
322322
323-func (f *Swarm) releaseCapacity(a *Agent) {
323+func (f *Supervisor) releaseCapacity(a *Agent) {
324324 f.mu.Lock()
325325 if f.active > 0 {
326326 f.active--
@@ -336,7 +336,7 @@ func (f *Swarm) releaseCapacity(a *Agent) {
336336 f.schedule()
337337 }
338338
339-func (f *Swarm) persistAgent(a *Agent) {
339+func (f *Supervisor) persistAgent(a *Agent) {
340340 if a == nil {
341341 return
342342 }
R packages/agent/swarm/scheduler_test.go → packages/agent/subagents/scheduler_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
R packages/agent/swarm/socketpath.go → packages/agent/subagents/socketpath.go +5 -5 renamed, 91% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "crypto/sha1"
@@ -18,7 +18,7 @@ import (
1818 const maxUnixSocketPath = 100
1919
2020 // inboxSocketPath returns a per-agent unix-socket path in a transient
21-// runtime directory. Durable swarm state can live on filesystems such as
21+// runtime directory. Durable subagent state can live on filesystems such as
2222 // NFS, 9p, or shared VM mounts that support regular files but reject unix
2323 // socket nodes, so the socket must not be placed below root.
2424 //
@@ -33,7 +33,7 @@ const maxUnixSocketPath = 100
3333 // stable path across parent and child processes and across Resume calls.
3434 func inboxSocketPath(root, agentID string) (string, error) {
3535 if runtime.GOOS == "windows" {
36- // Keep swarm construction usable for callers with custom runners.
36+ // Keep subagent construction usable for callers with custom runners.
3737 // The production listener still reports that its unix transport is
3838 // unavailable, as it did before runtime-directory probing was added.
3939 return filepath.Join(root, "agents", agentID, "in.sock"), nil
@@ -65,7 +65,7 @@ func inboxSocketPath(root, agentID string) (string, error) {
6565 }
6666
6767 func socketPathInBase(base, root, agentID string) (string, error) {
68- dir := filepath.Join(base, "zot-swarm-"+rootTag(root))
68+ dir := filepath.Join(base, "zot-subagents-"+rootTag(root))
6969 if err := os.MkdirAll(dir, 0o700); err != nil {
7070 return "", fmt.Errorf("socket dir %s: %w", dir, err)
7171 }
@@ -109,7 +109,7 @@ func probeUnixSocket(dir string) error {
109109 return errors.Join(closeErr, removeErr)
110110 }
111111
112-// rootTag returns a stable 8-hex-char tag for the swarm root. Used
112+// rootTag returns a stable 8-hex-char tag for the subagent root. Used
113113 // in the runtime-directory name so two parallel zot instances with
114114 // different roots don't share sockets.
115115 func rootTag(root string) string { return shortHash(root) }
R packages/agent/swarm/socketpath_test.go → packages/agent/subagents/socketpath_test.go +4 -4 renamed, 89% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "os"
@@ -10,7 +10,7 @@ import (
1010
1111 func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) {
1212 if runtime.GOOS == "windows" {
13- t.Skip("swarm inbox transport uses Unix-domain sockets")
13+ t.Skip("subagent inbox transport uses Unix-domain sockets")
1414 }
1515
1616 runtimeDir := shortSocketDir(t)
@@ -31,7 +31,7 @@ func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) {
3131
3232 func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) {
3333 if runtime.GOOS == "windows" {
34- t.Skip("swarm inbox transport uses Unix-domain sockets")
34+ t.Skip("subagent inbox transport uses Unix-domain sockets")
3535 }
3636
3737 badRuntime := filepath.Join(shortSocketDir(t), "not-a-directory")
@@ -53,7 +53,7 @@ func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) {
5353
5454 func TestInboxSocketPathShortensLongAgentID(t *testing.T) {
5555 if runtime.GOOS == "windows" {
56- t.Skip("swarm inbox transport uses Unix-domain sockets")
56+ t.Skip("subagent inbox transport uses Unix-domain sockets")
5757 }
5858
5959 t.Setenv("XDG_RUNTIME_DIR", shortSocketDir(t))
R packages/agent/swarm/swarm.go → packages/agent/subagents/supervisor.go +47 -90 renamed, 89% similar
@@ -1,17 +1,14 @@
1-// Package swarm implements zot's subagent supervisor.
1+// Package subagents implements zot's subagent supervisor.
22 //
3-// A Swarm manages headless, long-lived zot subprocesses and keeps their
3+// A Supervisor manages headless, long-lived zot subprocesses and keeps their
44 // process/turn state, event logs, sessions, results, and optional worktree
5-// artifacts durable across supervisor restarts. The canonical user-facing
6-// name is "subagents"; this package and the /swarm command remain for
7-// compatibility with existing callers.
5+// artifacts durable across supervisor restarts.
86 //
97 // Shared workspaces preserve the historical behavior, while an explicit
108 // worktree request gives a child an isolated checkout whose patch is captured
119 // before cleanup. The Runner abstraction lets tests inject a fake process;
12-// production uses `zot --subagent-worker ...` with the legacy flag accepted as
13-// a compatibility alias.
14-package swarm
10+// production uses `zot --subagent-worker ...`.
11+package subagents
1512
1613 import (
1714 "context"
@@ -38,7 +35,7 @@ const (
3835 StatusDetached Status = "detached" // reloaded from disk; no live runner
3936 )
4037
41-// Config configures a Swarm.
38+// Config configures a Supervisor.
4239 type Config struct {
4340 // Context is the supervisor lifetime. Child workers use it as their
4441 // parent context rather than inheriting an individual tool/model-turn
@@ -46,14 +43,9 @@ type Config struct {
4643 Context context.Context
4744
4845 // Root is the directory under which per-agent state files live.
49- // Typically <ZotHome>/swarm, but tests pass a tempdir.
46+ // Typically <ZotHome>/subagents, but tests pass a tempdir.
5047 Root string
5148
52- // LegacyRoots are older state roots that Reload also reads. New
53- // agents are always written below Root; this keeps the swarm ->
54- // subagents rename from making existing sessions disappear.
55- LegacyRoots []string
56-
5749 // Policy owns concurrency, timeout, output, and path safety limits.
5850 Policy SubagentPolicy
5951
@@ -78,7 +70,7 @@ type Config struct {
7870 InsecureTLS bool
7971
8072 // NewRunner produces the Runner for an Agent. If nil, the default
81- // `zot --swarm-agent ...` exec runner is used. Tests inject a fake
73+ // `zot --subagent-worker ...` exec runner is used. Tests inject a fake
8274 // here.
8375 NewRunner func(a *Agent) Runner
8476
@@ -91,7 +83,7 @@ type Config struct {
9183 Now func() time.Time
9284 }
9385
94-// Credential is the resolved authentication state inherited by a swarm child.
86+// Credential is the resolved authentication state inherited by a subagent child.
9587 type Credential struct {
9688 Value string `json:"value"`
9789 Method string `json:"method"`
@@ -103,7 +95,7 @@ type Credential struct {
10395 //
10496 // Run should report progress by writing short human-readable strings
10597 // to the activity channel and final transcript text to transcript.
106-// Both channels are non-blocking sinks owned by the Swarm; if the
98+// Both channels are non-blocking sinks owned by the Supervisor; if the
10799 // dashboard isn't reading, sends are dropped.
108100 type Runner interface {
109101 Run(ctx context.Context, sink Sink) error
@@ -121,8 +113,8 @@ type Sink interface {
121113 Transcript(chunk string)
122114 }
123115
124-// Swarm supervises a set of Agents.
125-type Swarm struct {
116+// Supervisor supervises a set of Agents.
117+type Supervisor struct {
126118 cfg Config
127119
128120 mu sync.Mutex
@@ -140,19 +132,19 @@ type Swarm struct {
140132
141133 // activeSession is the host session id the dashboard is
142134 // currently scoped to. When non-empty, SnapshotAll filters out
143- // agents whose SessionID doesn't match (legacy / unscoped
135+ // agents whose SessionID doesn't match (unscoped
144136 // agents with SessionID == "" are always shown). Spawn stamps
145137 // new agents with this value so they appear only in the session
146138 // that created them. When empty (the default), the historical
147139 // "show everything" behaviour is preserved — important for
148- // tests and any scripted use of the Swarm that doesn't bother
140+ // tests and any scripted use of the Supervisor that doesn't bother
149141 // with sessions.
150142 activeSession string
151143 }
152144
153-// New constructs a Swarm from cfg. Missing config fields are filled
145+// New constructs a Supervisor from cfg. Missing config fields are filled
154146 // with defaults.
155-func New(cfg Config) *Swarm {
147+func New(cfg Config) *Supervisor {
156148 if cfg.Now == nil {
157149 cfg.Now = time.Now
158150 }
@@ -167,7 +159,7 @@ func New(cfg Config) *Swarm {
167159 lifetimeParent = context.Background()
168160 }
169161 lifetimeCtx, lifetimeCancel := context.WithCancel(lifetimeParent)
170- return &Swarm{
162+ return &Supervisor{
171163 cfg: cfg,
172164 lifetimeCtx: lifetimeCtx,
173165 lifetimeCancel: lifetimeCancel,
@@ -181,7 +173,7 @@ func New(cfg Config) *Swarm {
181173 // SetRepoRoot updates the shared working directory for subsequently
182174 // spawned agents. Existing agents keep the directory they were started
183175 // with; callers use this when the host session changes cwd.
184-func (f *Swarm) workerContext(timeout time.Duration) (context.Context, context.CancelFunc) {
176+func (f *Supervisor) workerContext(timeout time.Duration) (context.Context, context.CancelFunc) {
185177 base := f.lifetimeCtx
186178 if base == nil {
187179 base = context.Background()
@@ -192,7 +184,7 @@ func (f *Swarm) workerContext(timeout time.Duration) (context.Context, context.C
192184 return context.WithCancel(base)
193185 }
194186
195-func (f *Swarm) SetRepoRoot(root string) {
187+func (f *Supervisor) SetRepoRoot(root string) {
196188 f.mu.Lock()
197189 defer f.mu.Unlock()
198190 f.cfg.RepoRoot = root
@@ -200,7 +192,7 @@ func (f *Swarm) SetRepoRoot(root string) {
200192
201193 // RepoRoot returns the working directory used by subsequently spawned
202194 // agents.
203-func (f *Swarm) RepoRoot() string {
195+func (f *Supervisor) RepoRoot() string {
204196 f.mu.Lock()
205197 defer f.mu.Unlock()
206198 return f.cfg.RepoRoot
@@ -209,14 +201,14 @@ func (f *Swarm) RepoRoot() string {
209201 // SetFastMode updates the fast-mode setting for subsequently spawned
210202 // children. Existing agents keep the setting captured at their spawn
211203 // boundary so resumes remain deterministic.
212-func (f *Swarm) SetFastMode(enabled bool) {
204+func (f *Supervisor) SetFastMode(enabled bool) {
213205 f.mu.Lock()
214206 f.cfg.FastMode = enabled
215207 f.mu.Unlock()
216208 }
217209
218210 // FastMode reports the setting used for subsequently spawned children.
219-func (f *Swarm) FastMode() bool {
211+func (f *Supervisor) FastMode() bool {
220212 f.mu.Lock()
221213 defer f.mu.Unlock()
222214 return f.cfg.FastMode
@@ -226,13 +218,13 @@ func (f *Swarm) FastMode() bool {
226218 // subsequently spawned children. Existing agents keep their persisted values
227219 // so a model/provider switch cannot silently change a running or resumed
228220 // worker's endpoint.
229-func (f *Swarm) SetProvider(provider string) {
221+func (f *Supervisor) SetProvider(provider string) {
230222 f.mu.Lock()
231223 f.cfg.Provider = strings.TrimSpace(provider)
232224 f.mu.Unlock()
233225 }
234226
235-func (f *Swarm) SetProviderSettings(baseURL string, insecureTLS bool) {
227+func (f *Supervisor) SetProviderSettings(baseURL string, insecureTLS bool) {
236228 f.mu.Lock()
237229 f.cfg.BaseURL = strings.TrimSpace(baseURL)
238230 f.cfg.InsecureTLS = insecureTLS
@@ -242,7 +234,7 @@ func (f *Swarm) SetProviderSettings(baseURL string, insecureTLS bool) {
242234 // ProviderSettings returns the settings used by subsequently spawned
243235 // children. It is primarily useful to host integrations that need to verify
244236 // the manager follows the active provider selection.
245-func (f *Swarm) ProviderSettings() (string, bool) {
237+func (f *Supervisor) ProviderSettings() (string, bool) {
246238 f.mu.Lock()
247239 defer f.mu.Unlock()
248240 return f.cfg.BaseURL, f.cfg.InsecureTLS
@@ -256,7 +248,7 @@ func (f *Swarm) ProviderSettings() (string, bool) {
256248 // applied at snapshot time changes. So swapping the active session
257249 // with /sessions instantly re-narrows the dashboard without
258250 // touching any agent state.
259-func (f *Swarm) SetActiveSession(id string) {
251+func (f *Supervisor) SetActiveSession(id string) {
260252 f.mu.Lock()
261253 defer f.mu.Unlock()
262254 f.activeSession = id
@@ -264,7 +256,7 @@ func (f *Swarm) SetActiveSession(id string) {
264256
265257 // ActiveSession returns the current scope, mostly for tests and
266258 // diagnostics. Empty means "no scope; show everything".
267-func (f *Swarm) ActiveSession() string {
259+func (f *Supervisor) ActiveSession() string {
268260 f.mu.Lock()
269261 defer f.mu.Unlock()
270262 return f.activeSession
@@ -279,11 +271,11 @@ func (f *Swarm) ActiveSession() string {
279271 //
280272 // The transient unix socket inbox lives in a probed runtime directory,
281273 // which may be on a different filesystem from this durable state.
282-func (f *Swarm) agentStateDir(id string) string {
274+func (f *Supervisor) agentStateDir(id string) string {
283275 return filepath.Join(f.cfg.Root, "agents", id)
284276 }
285277
286-func (f *Swarm) uniqueAgentIDLocked(base string) string {
278+func (f *Supervisor) uniqueAgentIDLocked(base string) string {
287279 id := base
288280 for suffix := 2; ; suffix++ {
289281 _, inMemory := f.agents[id]
@@ -340,14 +332,14 @@ type SpawnRequest struct {
340332 // when the concurrency budget is full and transitions to running when a slot
341333 // opens. This is the historical signature; callers that want to override the
342334 // child's model or workspace use SpawnReq instead.
343-func (f *Swarm) Spawn(ctx context.Context, task string) (*Agent, error) {
335+func (f *Supervisor) Spawn(ctx context.Context, task string) (*Agent, error) {
344336 return f.SpawnReq(ctx, SpawnRequest{Task: task})
345337 }
346338
347339 // SpawnReq is the full-fat variant of Spawn that accepts a SpawnRequest.
348340 // Existing callers can keep using Spawn; new code that wants to pin the
349341 // child's model, policy, or workspace uses this.
350-func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error) {
342+func (f *Supervisor) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error) {
351343 task := strings.TrimSpace(req.Task)
352344 if task == "" {
353345 return nil, errors.New("subagents: empty task")
@@ -538,27 +530,12 @@ func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error)
538530 return a, nil
539531 }
540532
541-// SendInput delivers msg to the agent's inbox. msg should already
542-// include the leading kind ("user " + text, "cancel", "shutdown").
543-// Returns an error wrapping ErrNotReady if the child hasn't opened
544-// its listener yet; the caller can retry or surface the wait.
545-func (f *Swarm) SendInput(id, msg string) error {
546- a := f.Get(id)
547- if a == nil {
548- return fmt.Errorf("no such agent %q", id)
549- }
550- if a.inbox == nil {
551- return fmt.Errorf("agent %s has no inbox", a.ID)
552- }
553- return a.inbox.SendInput(msg)
554-}
555-
556533 // SendUserTurn is sugar for the common "send the next user turn"
557534 // case. It quotes nothing and forwards verbatim; callers are
558535 // expected to have already trimmed and expanded the text.
559536 // CancelTurn requests cancellation of the current turn while keeping the
560537 // worker process alive for a later follow-up.
561-func (f *Swarm) CancelTurn(id string) error {
538+func (f *Supervisor) CancelTurn(id string) error {
562539 a := f.Get(id)
563540 if a == nil {
564541 return fmt.Errorf("subagents: no such agent %q", id)
@@ -567,13 +544,10 @@ func (f *Swarm) CancelTurn(id string) error {
567544 return fmt.Errorf("subagents: agent %s has no inbox", a.ID)
568545 }
569546 a.setTurnState(TurnCanceling, a.CurrentTurnID())
570- if !a.protocolReady() {
571- return a.inbox.SendInput("cancel")
572- }
573547 return a.inbox.SendCommand(NewCommand(CommandTurnCancel, a.ID, a.CurrentTurnID(), TurnCancelPayload{Reason: "user"}))
574548 }
575549
576-func (f *Swarm) SendUserTurn(id, text string) error {
550+func (f *Supervisor) SendUserTurn(id, text string) error {
577551 a := f.Get(id)
578552 if a == nil {
579553 return fmt.Errorf("subagents: no such agent %q", id)
@@ -581,16 +555,10 @@ func (f *Swarm) SendUserTurn(id, text string) error {
581555 if a.inbox == nil {
582556 return fmt.Errorf("subagents: agent %s has no inbox", a.ID)
583557 }
584- if !a.protocolReady() {
585- // Until agent.ready arrives, use the legacy spelling so a prompt
586- // sent immediately after Spawn is not lost during compatibility
587- // negotiation. The worker accepts both forms.
588- return a.inbox.SendInput("user " + text)
589- }
590558 return a.inbox.SendCommand(NewCommand(CommandTurnStart, a.ID, a.CurrentTurnID(), TurnStartPayload{Prompt: text}))
591559 }
592560
593-func (f *Swarm) run(a *Agent) {
561+func (f *Supervisor) run(a *Agent) {
594562 a.mu.Lock()
595563 killedBeforeStart := a.status == StatusKilled
596564 ctxErr := error(nil)
@@ -679,9 +647,9 @@ func (f *Swarm) run(a *Agent) {
679647
680648 // List returns a snapshot of every agent in creation order. The
681649 // returned slice is a copy; callers may iterate without holding the
682-// swarm lock. Agent fields are read under their own mutex during
650+// subagent lock. Agent fields are read under their own mutex during
683651 // formatting in Snapshot.
684-func (f *Swarm) List() []*Agent {
652+func (f *Supervisor) List() []*Agent {
685653 f.mu.Lock()
686654 defer f.mu.Unlock()
687655 out := make([]*Agent, 0, len(f.order))
@@ -694,7 +662,7 @@ func (f *Swarm) List() []*Agent {
694662 // Get returns the agent with the given (possibly truncated) id, or
695663 // nil. Matching is prefix-based so the user can type the first few
696664 // characters of a long id.
697-func (f *Swarm) Get(id string) *Agent {
665+func (f *Supervisor) Get(id string) *Agent {
698666 f.mu.Lock()
699667 defer f.mu.Unlock()
700668 if a, ok := f.agents[id]; ok {
@@ -716,7 +684,7 @@ func (f *Swarm) Get(id string) *Agent {
716684 // Stop cancels the agent's context. The Runner should observe the
717685 // cancellation and return promptly; the goroutine then finalises the
718686 // agent in StatusKilled. Also closes the supervisor-side inbox handle
719-// so any pending SendInput retries fail fast instead of dialing a
687+// so any pending send retries fail fast instead of dialing a
720688 // socket about to be unlinked.
721689 //
722690 // Stop is a no-op for any agent that's not in a live runnable state
@@ -724,7 +692,7 @@ func (f *Swarm) Get(id string) *Agent {
724692 // are probed first: if their worker survived the supervisor, Stop sends the
725693 // shutdown command through the durable inbox instead of pretending no
726694 // process exists.
727-func (f *Swarm) Stop(id string) error {
695+func (f *Supervisor) Stop(id string) error {
728696 f.operationMu.Lock()
729697 defer f.operationMu.Unlock()
730698 a := f.Get(id)
@@ -749,18 +717,12 @@ func (f *Swarm) Stop(id string) error {
749717 path := a.InboxPath
750718 inbox := a.inbox
751719 pid := a.ProcessPIDValue()
752- protocolReady := a.protocolReady()
753720 a.mu.Unlock()
754721 f.mu.Unlock()
755722 if inbox == nil || !inboxLive(path) {
756723 return nil
757724 }
758- var err error
759- if protocolReady {
760- err = inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{}))
761- } else {
762- err = inbox.SendInput("shutdown")
763- }
725+ err := inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{}))
764726 if err != nil {
765727 return err
766728 }
@@ -836,12 +798,7 @@ func (f *Swarm) Stop(id string) error {
836798 // safer than leaving a supervisor entry running forever.
837799 graceful := false
838800 if a.inbox != nil {
839- var err error
840- if a.protocolReady() {
841- err = a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{}))
842- } else {
843- err = a.inbox.SendInput("shutdown")
844- }
801+ err := a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{}))
845802 if err == nil {
846803 graceful = true
847804 }
@@ -873,7 +830,7 @@ func (f *Swarm) Stop(id string) error {
873830 }
874831
875832 // StopAll cancels every running agent. Used on shutdown.
876-func (f *Swarm) StopAll() {
833+func (f *Supervisor) StopAll() {
877834 agents := f.List()
878835 for _, a := range agents {
879836 _ = f.Stop(a.ID)
@@ -896,7 +853,7 @@ func (f *Swarm) StopAll() {
896853 // agents have already captured their output and removed the isolated checkout
897854 // during finalization. Remove then deletes the agent's durable state directory
898855 // under <root>/agents/<id>/.
899-func (f *Swarm) Remove(id string) error {
856+func (f *Supervisor) Remove(id string) error {
900857 f.operationMu.Lock()
901858 defer f.operationMu.Unlock()
902859 a := f.Get(id)
@@ -983,7 +940,7 @@ type AgentSnapshot struct {
983940 WorkspacePath string
984941
985942 // Paths to the agent's durable state. Surface them in the
986- // snapshot so the dashboard /subagents (or compatibility /swarm) view can
943+ // snapshot so the dashboard /subagents view can
987944 // read events.jsonl or resume the session without going back through the Agent.
988945 InboxPath string
989946 EventLogPath string
@@ -1043,10 +1000,10 @@ func (a *Agent) Snapshot() AgentSnapshot {
10431000 // - activeSession == "": no filter, every agent is returned
10441001 // (historical behaviour; used by tests and scripted callers).
10451002 // - activeSession != "": include live agents whose SessionID matches
1046-// activeSession, plus unscoped legacy agents. Detached and terminal
1003+// activeSession, plus unscoped agents. Detached and terminal
10471004 // agents from another host session remain visible so restart/recovery
10481005 // does not hide durable work.
1049-func (f *Swarm) SnapshotAll() []AgentSnapshot {
1006+func (f *Supervisor) SnapshotAll() []AgentSnapshot {
10501007 agents := f.List()
10511008 f.mu.Lock()
10521009 active := f.activeSession
@@ -1070,7 +1027,7 @@ func (f *Swarm) SnapshotAll() []AgentSnapshot {
10701027 return out
10711028 }
10721029
1073-// agentSink is the Sink the Swarm hands to each Runner.
1030+// agentSink is the Sink the Supervisor hands to each Runner.
10741031 type agentSink struct{ a *Agent }
10751032
10761033 func (s agentSink) Activity(msg string) { s.a.setActivity(msg) }
R packages/agent/swarm/swarm_test.go → packages/agent/subagents/supervisor_test.go +15 -15 renamed, 88% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
@@ -9,11 +9,11 @@ import (
99 "time"
1010 )
1111
12-// newTestSwarm builds a Swarm rooted in t.TempDir with the in-memory
13-// and a Runner factory the test controls. Returns the swarm plus a
12+// newTestSupervisor builds a Supervisor rooted in t.TempDir with the in-memory
13+// and a Runner factory the test controls. Returns the subagent plus a
1414 // slice of runners keyed by spawn order so tests can assert they
1515 // were actually invoked.
16-func newTestSwarm(t *testing.T, mk func(a *Agent) Runner) *Swarm {
16+func newTestSupervisor(t *testing.T, mk func(a *Agent) Runner) *Supervisor {
1717 t.Helper()
1818 root := t.TempDir()
1919 return New(Config{
@@ -25,7 +25,7 @@ func newTestSwarm(t *testing.T, mk func(a *Agent) Runner) *Swarm {
2525
2626 func TestSpawnRunsAndCompletes(t *testing.T) {
2727 ran := make(chan string, 1)
28- f := newTestSwarm(t, func(a *Agent) Runner {
28+ f := newTestSupervisor(t, func(a *Agent) Runner {
2929 return RunnerFunc(func(ctx context.Context, sink Sink) error {
3030 ran <- a.Task
3131 sink.Activity("hello")
@@ -65,7 +65,7 @@ func TestSpawnRunsAndCompletes(t *testing.T) {
6565 // TestSpawnAgentSharesRepoRoot verifies the only-mode-we-support:
6666 // every spawned agent points its cwd at the parent zot's RepoRoot.
6767 func TestSpawnAgentSharesRepoRoot(t *testing.T) {
68- f := newTestSwarm(t, func(a *Agent) Runner {
68+ f := newTestSupervisor(t, func(a *Agent) Runner {
6969 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
7070 })
7171 a, err := f.Spawn(context.Background(), "share me")
@@ -79,7 +79,7 @@ func TestSpawnAgentSharesRepoRoot(t *testing.T) {
7979 }
8080
8181 func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) {
82- f := newTestSwarm(t, func(a *Agent) Runner {
82+ f := newTestSupervisor(t, func(a *Agent) Runner {
8383 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
8484 })
8585 want := t.TempDir()
@@ -95,7 +95,7 @@ func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) {
9595 }
9696
9797 func TestSpawnEmptyTaskFails(t *testing.T) {
98- f := newTestSwarm(t, func(a *Agent) Runner {
98+ f := newTestSupervisor(t, func(a *Agent) Runner {
9999 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
100100 })
101101 if _, err := f.Spawn(context.Background(), " "); err == nil {
@@ -105,7 +105,7 @@ func TestSpawnEmptyTaskFails(t *testing.T) {
105105
106106 func TestRunnerErrorMarksFailed(t *testing.T) {
107107 wantErr := errors.New("boom")
108- f := newTestSwarm(t, func(a *Agent) Runner {
108+ f := newTestSupervisor(t, func(a *Agent) Runner {
109109 return RunnerFunc(func(ctx context.Context, sink Sink) error { return wantErr })
110110 })
111111 a, _ := f.Spawn(context.Background(), "explode")
@@ -123,7 +123,7 @@ func TestRunnerErrorMarksFailed(t *testing.T) {
123123
124124 func TestStopCancelsRunningAgent(t *testing.T) {
125125 started := make(chan struct{})
126- f := newTestSwarm(t, func(a *Agent) Runner {
126+ f := newTestSupervisor(t, func(a *Agent) Runner {
127127 return RunnerFunc(func(ctx context.Context, sink Sink) error {
128128 close(started)
129129 <-ctx.Done()
@@ -142,7 +142,7 @@ func TestStopCancelsRunningAgent(t *testing.T) {
142142 }
143143
144144 func TestStopAfterDoneIsNoop(t *testing.T) {
145- f := newTestSwarm(t, func(a *Agent) Runner {
145+ f := newTestSupervisor(t, func(a *Agent) Runner {
146146 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
147147 })
148148 a, _ := f.Spawn(context.Background(), "quick")
@@ -156,7 +156,7 @@ func TestStopAfterDoneIsNoop(t *testing.T) {
156156 }
157157
158158 func TestGetPrefixMatch(t *testing.T) {
159- f := newTestSwarm(t, func(a *Agent) Runner {
159+ f := newTestSupervisor(t, func(a *Agent) Runner {
160160 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
161161 })
162162 a, _ := f.Spawn(context.Background(), "alpha task")
@@ -177,7 +177,7 @@ func TestGetPrefixMatch(t *testing.T) {
177177
178178 func TestRemoveRequiresTerminalState(t *testing.T) {
179179 hold := make(chan struct{})
180- f := newTestSwarm(t, func(a *Agent) Runner {
180+ f := newTestSupervisor(t, func(a *Agent) Runner {
181181 return RunnerFunc(func(ctx context.Context, sink Sink) error {
182182 <-hold
183183 return nil
@@ -204,7 +204,7 @@ func TestRemoveRequiresTerminalState(t *testing.T) {
204204 func TestSnapshotIsStableAcrossAccess(t *testing.T) {
205205 var wg sync.WaitGroup
206206 wg.Add(1)
207- f := newTestSwarm(t, func(a *Agent) Runner {
207+ f := newTestSupervisor(t, func(a *Agent) Runner {
208208 return RunnerFunc(func(ctx context.Context, sink Sink) error {
209209 defer wg.Done()
210210 for i := 0; i < 200; i++ {
@@ -251,7 +251,7 @@ func TestTaskSlug(t *testing.T) {
251251 }
252252
253253 func TestSnapshotAllSorted(t *testing.T) {
254- f := newTestSwarm(t, func(a *Agent) Runner {
254+ f := newTestSupervisor(t, func(a *Agent) Runner {
255255 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
256256 })
257257 a1, _ := f.Spawn(context.Background(), "first")
R packages/agent/swarm/testdata/cmd/stubchild/main.go → packages/agent/subagents/testdata/cmd/stubchild/main.go +14 -25 renamed, 76% similar
@@ -1,8 +1,8 @@
1-// stubchild is a minimal swarm-agent stand-in used by the runner
1+// stubchild is a minimal subagent-worker stand-in used by the runner
22 // end-to-end test. It speaks the daemon protocol the real zot
33 // binary will implement next:
44 //
5-// - parses --swarm-agent <path>, --session <path>, --cwd <path>,
5+// - parses --subagent-worker <path>, --session <path>, --cwd <path>,
66 // and an optional positional task,
77 // - opens a unix-socket listener at the inbox path so the
88 // supervisor's Inbox can dial through,
@@ -13,7 +13,7 @@
1313 // so the runner sees the dialogue happen.
1414 //
1515 // The runner test compiles this binary into a tempdir, points
16-// swarm's execRunner at it via Command, and asserts the events
16+// subagent's execRunner at it via Command, and asserts the events
1717 // flow through correctly without needing the full zot model
1818 // machinery.
1919 package main
@@ -28,7 +28,7 @@ import (
2828 "sync"
2929 "time"
3030
31- "github.com/patriceckhart/zot/packages/agent/swarm"
31+ "github.com/patriceckhart/zot/packages/agent/subagents"
3232 )
3333
3434 func main() {
@@ -38,13 +38,12 @@ func main() {
3838 cwd string
3939 )
4040 flag.StringVar(&inboxPath, "subagent-worker", "", "inbox socket path")
41- flag.StringVar(&inboxPath, "swarm-agent", "", "compatibility inbox socket path")
4241 flag.StringVar(&sessionPath, "session", "", "session file path")
4342 flag.StringVar(&cwd, "cwd", "", "working directory")
4443 flag.Parse()
4544
4645 if inboxPath == "" {
47- fmt.Fprintln(os.Stderr, "stubchild: --swarm-agent required")
46+ fmt.Fprintln(os.Stderr, "stubchild: --subagent-worker required")
4847 os.Exit(2)
4948 }
5049
@@ -52,7 +51,7 @@ func main() {
5251
5352 // Open the inbox listener BEFORE doing any work so the supervisor
5453 // can dial through as soon as it sees the agent_ready event. If we
55- // processed the initial task first, the parent's SendInput call
54+ // processed the initial task first, the parent's send call
5655 // would race the stub's net.Listen and trip ErrNotReady.
5756 ln, err := net.Listen("unix", inboxPath)
5857 if err != nil {
@@ -84,35 +83,25 @@ func main() {
8483 line, err := br.ReadString('\n')
8584 if line != "" {
8685 msg := trimNL(line)
87- if command, parseErr := swarm.ParseCommand(msg); parseErr == nil {
86+ command, parseErr := subagents.ParseCommand(msg)
87+ if parseErr == nil {
8888 switch command.Type {
89- case swarm.CommandAgentShutdown:
89+ case subagents.CommandAgentShutdown:
9090 emit("agent_stopped", map[string]any{"reason": "shutdown"})
9191 _ = c.Close()
9292 return
93- case swarm.CommandTurnCancel:
93+ case subagents.CommandTurnCancel:
9494 emit("turn_end", map[string]any{"stop": "cancelled"})
95- case swarm.CommandTurnStart:
96- var payload swarm.TurnStartPayload
95+ case subagents.CommandTurnStart:
96+ var payload subagents.TurnStartPayload
9797 if command.DecodePayload(&payload) == nil {
9898 runTurn(emit, payload.Prompt, turn)
9999 turn++
100100 }
101101 }
102- } else {
103- switch {
104- case msg == "shutdown":
105- emit("agent_stopped", map[string]any{"reason": "shutdown"})
106- _ = c.Close()
107- return
108- case msg == "cancel":
109- emit("turn_end", map[string]any{"stop": "cancelled"})
110- case len(msg) > 5 && msg[:5] == "user ":
111- runTurn(emit, msg[5:], turn)
112- turn++
113- }
114102 }
115103 }
104+
116105 if err != nil {
117106 _ = c.Close()
118107 break
@@ -153,7 +142,7 @@ func newEmitter() emitter {
153142 data = map[string]any{}
154143 }
155144 if versioned {
156- envelope := swarm.NewEventEnvelope(typ, "stub-agent", "", data)
145+ envelope := subagents.NewEventEnvelope(typ, "stub-agent", "", data)
157146 _ = enc.Encode(envelope)
158147 return
159148 }
R packages/agent/swarm/workspace.go → packages/agent/subagents/workspace.go +2 -2 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "bytes"
@@ -49,7 +49,7 @@ type WorkspaceHandle interface {
4949 }
5050
5151 // SharedWorkspace leaves the host checkout untouched by the supervisor; a
52-// child may edit it exactly as older swarm workers did.
52+// child may edit it exactly as older subagent workers did.
5353 type SharedWorkspace struct {
5454 Root string
5555 }
R packages/agent/swarm/workspace_manager_test.go → packages/agent/subagents/workspace_manager_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
R packages/agent/swarm/workspace_test.go → packages/agent/subagents/workspace_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm
1+package subagents
22
33 import (
44 "context"
D packages/agent/swarm/aliases.go +0 -11
@@ -1,11 +0,0 @@
1-package swarm
2-
3-// Canonical domain names. The swarm package remains as a source-compatible
4-// implementation boundary while callers migrate to subagent terminology.
5-type SubagentManager = Swarm
6-type SubagentConfig = Config
7-type Subagent = Agent
8-type SubagentSnapshot = AgentSnapshot
9-type SubagentRequest = SpawnRequest
10-
11-func NewSubagentManager(cfg SubagentConfig) *SubagentManager { return New(cfg) }
R packages/agent/tools/swarm_spawn.go → packages/agent/tools/subagent_spawn.go +28 -41 renamed, 82% similar
@@ -8,29 +8,24 @@ import (
88 "time"
99
1010 "github.com/patriceckhart/zot/packages/agent/subagents"
11- "github.com/patriceckhart/zot/packages/agent/swarm"
1211 "github.com/patriceckhart/zot/packages/core"
1312 "github.com/patriceckhart/zot/packages/provider"
1413 )
1514
16-// SwarmSpawnTool lets the main agent fork a background sub-agent
17-// against the host's cwd via swarm.Swarm.SpawnReq. The sub-agent runs
15+// SubagentSpawnTool lets the main agent fork a background sub-agent
16+// against the host's cwd via subagents.Supervisor.SpawnReq. The sub-agent runs
1817 // in parallel: the tool returns the agent id immediately and the main
1918 // turn continues uninterrupted. The user can monitor / chat with the
20-// spawned agent via /swarm.
19+// spawned agent via /subagents.
2120 //
22-// Gated by the auto_swarm_enabled config flag at call time so a user
21+// Gated by the auto_subagents_enabled config flag at call time so a user
2322 // can flip it off mid-session and the next call refuses cleanly
2423 // without re-registering the tool.
25-type SwarmSpawnTool struct {
26- // ToolName selects the public spelling. Empty preserves the historical
27- // swarm_spawn alias; the CLI registers the canonical subagent_spawn name.
28- ToolName string
29-
30- // Swarm is the supervisor used to spawn agents. Nil means
31- // "auto-swarm not available in this mode" and the tool always
24+type SubagentSpawnTool struct {
25+ // Supervisor is the supervisor used to spawn agents. Nil means
26+ // "auto-subagents not available in this mode" and the tool always
3227 // errors.
33- Swarm *swarm.Swarm
28+ Supervisor *subagents.Supervisor
3429
3530 // Enabled reads the live config flag. Lets users toggle from
3631 // /settings without rebuilding the agent. When nil, the tool
@@ -39,7 +34,7 @@ type SwarmSpawnTool struct {
3934
4035 // DefaultModel and DefaultProvider return the host agent's resolved
4136 // model and provider. They are used when the tool call omits both
42- // fields and does not select a named profile, so auto-swarm follows
37+ // fields and does not select a named profile, so auto-subagents follows
4338 // the same auth route as the user sees in the parent session.
4439 DefaultModel func() string
4540 DefaultProvider func() string
@@ -53,14 +48,10 @@ type SwarmSpawnTool struct {
5348 // the new agent + the task it was started with. Used by the
5449 // interactive host to track agents and surface a summary back
5550 // in chat when all sub-agents finish.
56- OnSpawned func(agent *swarm.Agent, task string)
51+ OnSpawned func(agent *subagents.Agent, task string)
5752 }
5853
59-// SubagentSpawnTool is the canonical spelling; SwarmSpawnTool remains an
60-// alias for integrations that used the original API.
61-type SubagentSpawnTool = SwarmSpawnTool
62-
63-type swarmSpawnArgs struct {
54+type subagentSpawnArgs struct {
6455 Task string `json:"task"`
6556 Agent string `json:"agent,omitempty"`
6657 Model string `json:"model,omitempty"`
@@ -72,7 +63,7 @@ type swarmSpawnArgs struct {
7263 MaxTurns int `json:"max_turns,omitempty"`
7364 }
7465
75-const swarmSpawnSchema = `{
66+const subagentSpawnSchema = `{
7667 "type": "object",
7768 "properties": {
7869 "task": {
@@ -104,7 +95,7 @@ const swarmSpawnSchema = `{
10495 "isolation": {
10596 "type": "string",
10697 "enum": ["shared", "worktree"],
107- "description": "Workspace mode. Shared preserves legacy behavior; worktree captures a patch without merging it."
98+ "description": "Workspace mode. Shared preserves existing behavior; worktree captures a patch without merging it."
10899 },
109100 "timeout": {
110101 "type": "string",
@@ -119,26 +110,22 @@ const swarmSpawnSchema = `{
119110 "required": ["task"]
120111 }`
121112
122-func (t *SwarmSpawnTool) Name() string {
123- if strings.TrimSpace(t.ToolName) != "" {
124- return strings.TrimSpace(t.ToolName)
125- }
126- return "swarm_spawn"
127-}
128-func (t *SwarmSpawnTool) Description() string {
129- return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile, model/provider/reasoning, timeout, turn limit, shared/worktree isolation, and fast-mode preference. Returns the sub-agent id immediately; completion arrives through the result/event path."
113+func (t *SubagentSpawnTool) Name() string { return "subagent_spawn" }
114+func (t *SubagentSpawnTool) Description() string {
115+ return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile, model/provider/reasoning, timeout, turn limit, and shared/worktree isolation, and fast-mode preference. Returns the sub-agent id immediately; completion arrives through the result/event path."
116+
130117 }
131-func (t *SwarmSpawnTool) Schema() json.RawMessage { return json.RawMessage(swarmSpawnSchema) }
118+func (t *SubagentSpawnTool) Schema() json.RawMessage { return json.RawMessage(subagentSpawnSchema) }
132119
133-func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
120+func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
134121 prefix := t.Name()
135- if t.Swarm == nil {
136- return protocolToolError(prefix + ": swarm supervisor not available in this mode")
122+ if t.Supervisor == nil {
123+ return protocolToolError(prefix + ": subagent supervisor not available in this mode")
137124 }
138125 if t.Enabled == nil || !t.Enabled() {
139- return protocolToolError(prefix + ": auto-swarm is disabled. Ask the user to enable it from /settings before delegating sub-tasks.")
126+ return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before delegating sub-tasks.")
140127 }
141- var a swarmSpawnArgs
128+ var a subagentSpawnArgs
142129 if err := json.Unmarshal(raw, &a); err != nil {
143130 return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)
144131 }
@@ -147,10 +134,10 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
147134 return protocolToolError(prefix + ": task is required")
148135 }
149136
150- workspaceMode := swarm.WorkspaceShared
137+ workspaceMode := subagents.WorkspaceShared
151138 if value := strings.TrimSpace(a.Isolation); value != "" {
152- workspaceMode = swarm.WorkspaceMode(value)
153- if workspaceMode != swarm.WorkspaceShared && workspaceMode != swarm.WorkspaceWorktree {
139+ workspaceMode = subagents.WorkspaceMode(value)
140+ if workspaceMode != subagents.WorkspaceShared && workspaceMode != subagents.WorkspaceWorktree {
154141 return protocolToolError(prefix + ": isolation must be shared or worktree")
155142 }
156143 }
@@ -230,7 +217,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
230217 }
231218 }
232219
233- agent, err := t.Swarm.SpawnReq(ctx, swarm.SpawnRequest{
220+ agent, err := t.Supervisor.SpawnReq(ctx, subagents.SpawnRequest{
234221 Task: task,
235222 Model: model,
236223 Provider: providerID,
@@ -287,7 +274,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
287274 "state": agent.Status(),
288275 "process_state": string(agent.ProcessState()),
289276 "turn_state": string(agent.TurnState()),
290- "result_ref": swarm.ResultRef(agent.ID),
277+ "result_ref": subagents.ResultRef(agent.ID),
291278 },
292279 }, nil
293280 }
R packages/agent/tools/swarm_spawn_test.go → packages/agent/tools/subagent_spawn_test.go +39 -39 renamed, 76% similar
@@ -8,31 +8,30 @@ import (
88 "testing"
99
1010 "github.com/patriceckhart/zot/packages/agent/subagents"
11- "github.com/patriceckhart/zot/packages/agent/swarm"
1211 "github.com/patriceckhart/zot/packages/provider"
1312 )
1413
15-type noopSwarmRunner struct{}
14+type noopSupervisorRunner struct{}
1615
17-func (noopSwarmRunner) Run(context.Context, swarm.Sink) error { return nil }
16+func (noopSupervisorRunner) Run(context.Context, subagents.Sink) error { return nil }
1817
19-func newTestSwarm(t *testing.T) *swarm.Swarm {
18+func newTestSupervisor(t *testing.T) *subagents.Supervisor {
2019 t.Helper()
2120 root := t.TempDir()
22- manager := swarm.New(swarm.Config{
23- Root: filepath.Join(root, "swarm"),
21+ manager := subagents.New(subagents.Config{
22+ Root: filepath.Join(root, "subagents"),
2423 RepoRoot: root,
25- NewRunner: func(*swarm.Agent) swarm.Runner {
26- return noopSwarmRunner{}
24+ NewRunner: func(*subagents.Agent) subagents.Runner {
25+ return noopSupervisorRunner{}
2726 },
2827 })
2928 t.Cleanup(manager.StopAll)
3029 return manager
3130 }
3231
33-func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {
34- tool := &SwarmSpawnTool{
35- Swarm: newTestSwarm(t),
32+func TestSubagentSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {
33+ tool := &SubagentSpawnTool{
34+ Supervisor: newTestSupervisor(t),
3635 Enabled: func() bool { return true },
3736 DefaultModel: func() string { return "gpt-5" },
3837 DefaultProvider: func() string { return "openai-codex" },
@@ -64,7 +63,7 @@ func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {
6463 t.Fatalf("result text missing inherited model/provider:\n%s", text)
6564 }
6665
67- agents := tool.Swarm.List()
66+ agents := tool.Supervisor.List()
6867 if len(agents) != 1 {
6968 t.Fatalf("spawned agents = %d, want 1", len(agents))
7069 }
@@ -73,10 +72,10 @@ func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {
7372 }
7473 }
7574
76-func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) {
77- tool := &SwarmSpawnTool{
78- Swarm: newTestSwarm(t),
79- Enabled: func() bool { return true },
75+func TestSubagentSpawnSelectsProfileAndReasoning(t *testing.T) {
76+ tool := &SubagentSpawnTool{
77+ Supervisor: newTestSupervisor(t),
78+ Enabled: func() bool { return true },
8079 ResolveSubagent: func(name string) (*subagents.Profile, error) {
8180 if name != "reviewer" {
8281 return nil, nil
@@ -102,7 +101,7 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) {
102101 t.Fatalf("result text missing %q:\n%s", want, text)
103102 }
104103 }
105- agents := tool.Swarm.List()
104+ agents := tool.Supervisor.List()
106105 if len(agents) != 1 || agents[0].Subagent != "reviewer" || agents[0].Reasoning != "high" {
107106 t.Fatalf("agent profile/reasoning = %#v", agents)
108107 }
@@ -111,20 +110,21 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) {
111110 }
112111 }
113112
114-func TestSwarmSpawnAppliesProfileFastModeRestriction(t *testing.T) {
113+func TestSubagentSpawnAppliesProfileFastModeRestriction(t *testing.T) {
115114 root := t.TempDir()
116115 profileFastMode := false
117- f := swarm.New(swarm.Config{
118- Root: filepath.Join(root, "swarm"),
116+ f := subagents.New(subagents.Config{
117+ Root: filepath.Join(root, "subagents"),
119118 RepoRoot: root,
120119 FastMode: true,
121- NewRunner: func(*swarm.Agent) swarm.Runner {
122- return noopSwarmRunner{}
120+ NewRunner: func(*subagents.Agent) subagents.Runner {
121+ return noopSupervisorRunner{}
123122 },
124123 })
125- tool := &SwarmSpawnTool{
126- Swarm: f,
127- Enabled: func() bool { return true },
124+ t.Cleanup(f.StopAll)
125+ tool := &SubagentSpawnTool{
126+ Supervisor: f,
127+ Enabled: func() bool { return true },
128128 ResolveSubagent: func(name string) (*subagents.Profile, error) {
129129 if name != "reviewer" {
130130 return nil, nil
@@ -149,10 +149,10 @@ func TestSwarmSpawnAppliesProfileFastModeRestriction(t *testing.T) {
149149 }
150150 }
151151
152-func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
153- tool := &SwarmSpawnTool{
154- Swarm: newTestSwarm(t),
155- Enabled: func() bool { return true },
152+func TestSubagentSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
153+ tool := &SubagentSpawnTool{
154+ Supervisor: newTestSupervisor(t),
155+ Enabled: func() bool { return true },
156156 ResolveSubagent: func(name string) (*subagents.Profile, error) {
157157 if name != "reviewer" {
158158 return nil, nil
@@ -172,16 +172,16 @@ func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
172172 if res.IsError {
173173 t.Fatalf("unexpected tool error: %s", textResult(res.Content))
174174 }
175- agents := tool.Swarm.List()
175+ agents := tool.Supervisor.List()
176176 if len(agents) != 1 || agents[0].Reasoning != "low" {
177177 t.Fatalf("agent reasoning = %#v, want low", agents)
178178 }
179179 }
180180
181-func TestSwarmSpawnRejectsUnknownProfile(t *testing.T) {
182- tool := &SwarmSpawnTool{
183- Swarm: newTestSwarm(t),
184- Enabled: func() bool { return true },
181+func TestSubagentSpawnRejectsUnknownProfile(t *testing.T) {
182+ tool := &SubagentSpawnTool{
183+ Supervisor: newTestSupervisor(t),
184+ Enabled: func() bool { return true },
185185 ResolveSubagent: func(string) (*subagents.Profile, error) {
186186 return nil, nil
187187 },
@@ -193,14 +193,14 @@ func TestSwarmSpawnRejectsUnknownProfile(t *testing.T) {
193193 if !res.IsError || !strings.Contains(textResult(res.Content), "unknown subagent profile") {
194194 t.Fatalf("result = %#v, want unknown profile error", res)
195195 }
196- if got := len(tool.Swarm.List()); got != 0 {
196+ if got := len(tool.Supervisor.List()); got != 0 {
197197 t.Fatalf("spawned agents = %d, want 0", got)
198198 }
199199 }
200200
201-func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) {
202- tool := &SwarmSpawnTool{
203- Swarm: newTestSwarm(t),
201+func TestSubagentSpawnRejectsPartialModelProviderOverride(t *testing.T) {
202+ tool := &SubagentSpawnTool{
203+ Supervisor: newTestSupervisor(t),
204204 Enabled: func() bool { return true },
205205 DefaultModel: func() string { return "gpt-5" },
206206 DefaultProvider: func() string { return "openai-codex" },
@@ -216,7 +216,7 @@ func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) {
216216 if got := textResult(res.Content); !strings.Contains(got, "omit both model/provider") {
217217 t.Fatalf("error text = %q", got)
218218 }
219- if got := len(tool.Swarm.List()); got != 0 {
219+ if got := len(tool.Supervisor.List()); got != 0 {
220220 t.Fatalf("spawned agents = %d, want 0", got)
221221 }
222222 }
M packages/core/core_test.go +2 -2
@@ -116,11 +116,11 @@ func TestSessionRoundTripPreservesThoughtSignatures(t *testing.T) {
116116 }
117117 }
118118
119-// TestNewSessionAtPathCreatesAtExplicitPath proves the swarm child's
119+// TestNewSessionAtPathCreatesAtExplicitPath proves the subagent worker's
120120 // session-persistence fallback works: NewSessionAtPath creates the
121121 // file (and parent dirs) at the exact path the caller chose,
122122 // independent of SessionsDir(root, cwd). Without this helper, the
123-// swarm child's --session <path> with a non-existent path would
123+// subagent worker's --session <path> with a non-existent path would
124124 // fall through to NewSession and write the session under a
125125 // different (autogenerated) name, leaving the supervisor-chosen
126126 // path empty.
M packages/core/session.go +1 -1
@@ -153,7 +153,7 @@ func NewSession(root, cwd, providerName, model, version string) (*Session, error
153153 }
154154
155155 // NewSessionAtPath creates a session at an explicit file path. Used
156-// by callers (notably the swarm-agent child) that need the session
156+// by callers (notably the subagent worker) that need the session
157157 // file to live at a path chosen by their parent rather than under
158158 // SessionsDir. Returns an error if the file already exists — use
159159 // OpenSession for that case.
M packages/tui/render.go +1 -1
@@ -476,7 +476,7 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int
476476 // alternative — always writing SeqHideCursor + cursor-position +
477477 // SeqShowCursor — resets the terminal's cursor blink timer on
478478 // every tick. At our 120ms animation cadence that means the
479- // caret in an idle dialog editor (e.g. an open swarm transcript
479+ // caret in an idle dialog editor (e.g. an open subagent transcript
480480 // for an agent that's currently idle) appears as a solid block
481481 // that never blinks, because we keep "showing" it before the
482482 // terminal can blink it off. Bailing out here lets the OS run
M packages/tui/render_test.go +1 -1
@@ -14,7 +14,7 @@ import (
1414 // used to always emit SeqHideCursor + cursor-position +
1515 // SeqShowCursor, which resets the terminal's blink timer. Faster
1616 // than the OS blink interval, so an idle dialog editor (e.g. a
17-// re-opened swarm transcript whose agent isn't producing output)
17+// re-opened subagent transcript whose agent isn't producing output)
1818 // rendered the caret as a solid non-blinking block.
1919 //
2020 // With the no-op fast path the renderer leaves the screen alone

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close