tmux-team 2.1.0 → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **The lightweight coordination layer for terminal-based AI agents.**
4
4
 
5
- tmux-team is a protocol-agnostic "Team Lead" that enables multi-agent collaboration directly within your existing tmux workflow. It provides the transport layer, synchronization, and project management needed to turn a collection of isolated terminal panes into a coordinated AI task force.
5
+ tmux-team is a protocol-agnostic transport layer that enables multi-agent collaboration directly within your existing tmux workflow. It turns a collection of isolated terminal panes into a coordinated AI team.
6
6
 
7
7
  ---
8
8
 
@@ -24,7 +24,7 @@ Unlike heavyweight frameworks that require specific SDKs or cloud infrastructure
24
24
  - **Model Agnostic** — Works with Claude Code, Gemini CLI, Codex, Aider, or any CLI tool
25
25
  - **Zero Infrastructure** — No servers, no MCP setup, no complex configuration. If it runs in tmux, tmux-team can talk to it
26
26
  - **Whitelist-Friendly** — A single `tmux-team talk:*` prefix covers all operations, keeping AI tool permissions simple and safe
27
- - **Local-First** — Per-project `tmux-team.json` lives with your repo; global config in `~/.tmux-team/` (v2)
27
+ - **Local-First** — Per-project `tmux-team.json` lives with your repo; global config in `~/.config/tmux-team/`
28
28
 
29
29
  ---
30
30
 
@@ -34,32 +34,24 @@ Unlike heavyweight frameworks that require specific SDKs or cloud infrastructure
34
34
 
35
35
  ### 1. Deterministic Transport (`--delay` vs. `sleep`)
36
36
 
37
- **The Problem**: Tool allowlists typically approve one safe command (`tmux-team talk ...`) but not arbitrary shell commands. Using `sleep` is often blocked by security policies, requires shell availability and proper quoting, and creates a separate process that's hard to manage.
37
+ **The Problem**: Tool allowlists typically approve one safe command (`tmux-team talk ...`) but not arbitrary shell commands. Using `sleep` is often blocked by security policies.
38
38
 
39
- **The Why**: Internal delay keeps the workflow as a single tool call. This guarantees "First-Packet Integrity"—the CLI validates units (`500ms`, `2s`) and ensures the TTY buffer is ready to receive input specifically for that agent. No shell dependency, no policy friction.
39
+ **The Why**: Internal delay keeps the workflow as a single tool call. No shell dependency, no policy friction.
40
40
 
41
41
  ### 2. Stateless Handshakes (The "Nonce" Strategy)
42
42
 
43
- **The Problem**: Terminal panes are streams, not RPC channels. A simple `[DONE]` string could already be in scrollback, or the agent might say "I'm almost done" and trigger a false positive.
43
+ **The Problem**: Terminal panes are streams, not RPC channels. A simple `[DONE]` string could already be in scrollback.
44
44
 
45
- **The Why**: We use a unique **Nonce** (Number used once) for every request: `{tmux-team-end:8f3a}`.
45
+ **The Why**: We use a unique **Nonce** for every request: `{tmux-team-end:8f3a}`.
46
46
  - **Collision Avoidance** — Prevents matching markers from previous turns
47
- - **Completion Safety** — Ensures the agent has truly finished, not just paused mid-response
48
- - **Zero-API RPC** — Creates request/response semantics over a standard TTY without requiring agents to support a special protocol
49
-
50
- Combined with one-at-a-time locking, nonce markers ensure state stays consistent and debuggable.
47
+ - **Completion Safety** — Ensures the agent has truly finished
48
+ - **Zero-API RPC** — Creates request/response semantics over a standard TTY
51
49
 
52
50
  ### 3. Context Injection (Preambles)
53
51
 
54
- **The Problem**: AI agents are prone to "instruction drift." Over a long session, they might stop using your preferred format or forget constraints. Manually re-typing system instructions is a "token tax" on your own brain.
55
-
56
- **The Why**: Preambles act as a forced system prompt for CLI environments. By injecting these "hidden instructions" at the transport level, we ensure the agent remains in character (e.g., "You are the code reviewer, do not edit files") without cluttering the human's command history. It's about reducing **Cognitive Load**—the human focuses on intent, the CLI enforces protocol.
57
-
58
- ### 4. Token-Efficient Polling
59
-
60
- **The Problem**: The `--wait` feature is powerful but higher-risk: long-running commands, more state to manage, potential for hung processes.
52
+ **The Problem**: AI agents are prone to "instruction drift." Over a long session, they might forget constraints.
61
53
 
62
- **The Why**: Default to the simple mental model (send manually check). Teams opt into `--wait` when they're ready. By capturing only the last few lines of the buffer and searching for the short, high-entropy nonce, we keep overhead near zero—we're looking for a single "heartbeat" at the TTY's edge, not re-parsing the whole history.
54
+ **The Why**: Preambles act as a forced system prompt for CLI environments. By injecting these "hidden instructions" at the transport level, we ensure the agent remains in character.
63
55
 
64
56
  ---
65
57
 
@@ -69,7 +61,7 @@ Combined with one-at-a-time locking, nonce markers ensure state stays consistent
69
61
  npm install -g tmux-team
70
62
  ```
71
63
 
72
- **Requirements:** Node.js >= 16, tmux, macOS/Linux
64
+ **Requirements:** Node.js >= 18, tmux, macOS/Linux
73
65
 
74
66
  ### Shell Completion
75
67
 
@@ -139,10 +131,8 @@ Once the plugin is installed, coordinate directly from your Claude Code session:
139
131
  | `update <name> --pane/--remark` | Update agent configuration |
140
132
  | `remove <name>` | Unregister an agent |
141
133
  | `init` | Create `tmux-team.json` in current directory |
142
- | `pm init --name "Project"` | Initialize project management |
143
- | `pm m add/list/done` | Manage milestones |
144
- | `pm t add/list/update/done` | Manage tasks |
145
- | `pm log` | View audit event log |
134
+ | `config [show/set/clear]` | View/modify settings |
135
+ | `preamble [show/set/clear]` | Manage agent preambles |
146
136
  | `completion [zsh\|bash]` | Output shell completion script |
147
137
 
148
138
  ---
@@ -151,36 +141,58 @@ Once the plugin is installed, coordinate directly from your Claude Code session:
151
141
 
152
142
  ### Local Config (`./tmux-team.json`)
153
143
 
154
- Per-project agent registry:
144
+ Per-project agent registry with optional preambles:
155
145
 
156
146
  ```json
157
147
  {
158
- "claude": { "pane": "10.0", "remark": "Frontend specialist" },
159
- "codex": { "pane": "10.1", "remark": "Backend engineer" }
148
+ "claude": {
149
+ "pane": "10.0",
150
+ "remark": "Frontend specialist",
151
+ "preamble": "Focus on UI components. Ask for review before merging."
152
+ },
153
+ "codex": {
154
+ "pane": "10.1",
155
+ "remark": "Code reviewer",
156
+ "preamble": "You are the code quality guard. Review changes thoroughly."
157
+ }
160
158
  }
161
159
  ```
162
160
 
161
+ | Field | Description |
162
+ |-------|-------------|
163
+ | `pane` | tmux pane ID (required) |
164
+ | `remark` | Description shown in `list` |
165
+ | `preamble` | Hidden instructions prepended to every message |
166
+
163
167
  ### Global Config (`~/.config/tmux-team/config.json`)
164
168
 
169
+ Global settings that apply to all projects:
170
+
165
171
  ```json
166
172
  {
167
173
  "mode": "polling",
168
174
  "preambleMode": "always",
169
175
  "defaults": {
170
- "timeout": 60000,
171
- "pollInterval": 1000
172
- },
173
- "agents": {
174
- "gemini": {
175
- "preamble": "Do not edit files until explicitly asked."
176
- }
176
+ "timeout": 180,
177
+ "pollInterval": 1,
178
+ "captureLines": 100,
179
+ "preambleEvery": 3
177
180
  }
178
181
  }
179
182
  ```
180
183
 
184
+ | Field | Description |
185
+ |-------|-------------|
186
+ | `mode` | Default mode: `polling` (manual check) or `wait` (auto-wait) |
187
+ | `preambleMode` | `always` (inject preambles) or `disabled` |
188
+ | `defaults.timeout` | Default --wait timeout in seconds |
189
+ | `defaults.pollInterval` | Polling interval in seconds |
190
+ | `defaults.captureLines` | Default lines for `check` command |
191
+ | `defaults.preambleEvery` | Inject preamble every N messages (default: 3) |
192
+
181
193
  ---
182
194
 
183
- ## ✨ v2 Features
195
+ ## ✨ Features
184
196
 
185
197
  ### 📡 Enhanced `talk` Command
186
198
 
@@ -194,32 +206,23 @@ tmux-team talk codex "message" --wait --timeout 60
194
206
 
195
207
  ### 📜 Agent Preambles
196
208
 
197
- Inject hidden instructions into every message:
209
+ Inject hidden instructions into every message via local `tmux-team.json`:
198
210
 
199
211
  ```json
200
212
  {
201
- "agents": {
202
- "gemini": {
203
- "preamble": "Always explain your reasoning. Do not edit files directly."
204
- }
213
+ "gemini": {
214
+ "pane": "10.2",
215
+ "preamble": "Always explain your reasoning. Do not edit files directly."
205
216
  }
206
217
  }
207
218
  ```
208
219
 
209
- ### 🎯 Project Management
220
+ Use the CLI to manage preambles:
210
221
 
211
222
  ```bash
212
- # Initialize a team project
213
- tmux-team pm init --name "Auth Refactor"
214
-
215
- # Manage milestones and tasks
216
- tmux-team pm m add "MVP Release"
217
- tmux-team pm t add "Implement login" --milestone 1
218
- tmux-team pm t update 1 --status in_progress
219
- tmux-team pm t done 1
220
-
221
- # View audit log
222
- tmux-team pm log --limit 10
223
+ tmux-team preamble show gemini # View current preamble
224
+ tmux-team preamble set gemini "..." # Set preamble
225
+ tmux-team preamble clear gemini # Remove preamble
223
226
  ```
224
227
 
225
228
  ---
@@ -228,16 +231,49 @@ tmux-team pm log --limit 10
228
231
 
229
232
  tmux-team intentionally stays lightweight:
230
233
 
231
- - **Not an orchestrator** — No automatic task routing or agent selection
232
- - **Not a session manager** — Doesn't create/manage tmux sessions or git worktrees
234
+ - **Not an orchestrator** — No automatic agent selection or routing
235
+ - **Not a session manager** — Doesn't create/manage tmux sessions
233
236
  - **Not an LLM wrapper** — Doesn't process or route messages through AI
234
237
 
235
238
  It's the plumbing layer that lets humans and AI agents coordinate via tmux, nothing more.
236
239
 
237
240
  ---
238
241
 
242
+ ## 📖 Command Reference
243
+
244
+ ### talk Options
245
+
246
+ | Option | Description |
247
+ |--------|-------------|
248
+ | `--delay <seconds>` | Wait before sending (whitelist-friendly alternative to `sleep`) |
249
+ | `--wait` | Block until agent responds (nonce-based completion detection) |
250
+ | `--timeout <seconds>` | Max wait time (default: 180s) |
251
+ | `--no-preamble` | Skip agent preamble for this message |
252
+
253
+ ### config Command
254
+
255
+ ```bash
256
+ tmux-team config show # Show current config
257
+ tmux-team config set mode wait # Enable wait mode
258
+ tmux-team config set preambleMode disabled # Disable preambles
259
+ tmux-team config set preambleEvery 5 # Inject preamble every 5 messages
260
+ tmux-team config clear <key> # Clear a config value
261
+ ```
262
+
263
+ ### preamble Command
264
+
265
+ ```bash
266
+ tmux-team preamble show <agent> # Show agent's preamble
267
+ tmux-team preamble set <agent> "..." # Set agent's preamble
268
+ tmux-team preamble clear <agent> # Clear agent's preamble
269
+ ```
270
+
271
+ ---
272
+
239
273
  *Built for developers who live in the terminal and want their AIs to do the same.*
240
274
 
275
+ ---
276
+
241
277
  ## License
242
278
 
243
279
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-team",
3
- "version": "2.1.0",
3
+ "version": "3.0.0-alpha.1",
4
4
  "description": "CLI tool for AI agent collaboration in tmux - manage cross-pane communication",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -17,7 +17,6 @@ import { cmdRemove } from './commands/remove.js';
17
17
  import { cmdTalk } from './commands/talk.js';
18
18
  import { cmdCheck } from './commands/check.js';
19
19
  import { cmdCompletion } from './commands/completion.js';
20
- import { cmdPm } from './pm/commands.js';
21
20
  import { cmdConfig } from './commands/config.js';
22
21
  import { cmdPreamble } from './commands/preamble.js';
23
22
 
@@ -203,10 +202,6 @@ function main(): void {
203
202
  cmdCheck(ctx, args[0], args[1] ? parseInt(args[1], 10) : undefined);
204
203
  break;
205
204
 
206
- case 'pm':
207
- await cmdPm(ctx, args);
208
- break;
209
-
210
205
  case 'config':
211
206
  cmdConfig(ctx, args);
212
207
  break;
@@ -3,9 +3,9 @@
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
5
  import fs from 'fs';
6
- import type { Context } from '../types.js';
6
+ import type { Context, PaneEntry } from '../types.js';
7
7
  import { ExitCodes } from '../exits.js';
8
- import { saveLocalConfig } from '../config.js';
8
+ import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
9
9
 
10
10
  export function cmdAdd(ctx: Context, name: string, pane: string, remark?: string): void {
11
11
  const { ui, config, paths, flags, exit } = ctx;
@@ -23,12 +23,16 @@ export function cmdAdd(ctx: Context, name: string, pane: string, remark?: string
23
23
  exit(ExitCodes.ERROR);
24
24
  }
25
25
 
26
- config.paneRegistry[name] = { pane };
26
+ // Load existing config to preserve all fields (preamble, deny, etc.)
27
+ const localConfig = loadLocalConfigFile(paths);
28
+
29
+ const newEntry: PaneEntry = { pane };
27
30
  if (remark) {
28
- config.paneRegistry[name].remark = remark;
31
+ newEntry.remark = remark;
29
32
  }
33
+ localConfig[name] = newEntry;
30
34
 
31
- saveLocalConfig(paths, config.paneRegistry);
35
+ saveLocalConfigFile(paths, localConfig);
32
36
 
33
37
  if (flags.json) {
34
38
  ui.json({ added: name, pane, remark });
@@ -13,11 +13,15 @@ import {
13
13
  clearLocalSettings,
14
14
  } from '../config.js';
15
15
 
16
- type ConfigKey = 'mode' | 'preambleMode';
16
+ type EnumConfigKey = 'mode' | 'preambleMode';
17
+ type NumericConfigKey = 'preambleEvery';
18
+ type ConfigKey = EnumConfigKey | NumericConfigKey;
17
19
 
18
- const VALID_KEYS: ConfigKey[] = ['mode', 'preambleMode'];
20
+ const ENUM_KEYS: EnumConfigKey[] = ['mode', 'preambleMode'];
21
+ const NUMERIC_KEYS: NumericConfigKey[] = ['preambleEvery'];
22
+ const VALID_KEYS: ConfigKey[] = [...ENUM_KEYS, ...NUMERIC_KEYS];
19
23
 
20
- const VALID_VALUES: Record<ConfigKey, string[]> = {
24
+ const VALID_VALUES: Record<EnumConfigKey, string[]> = {
21
25
  mode: ['polling', 'wait'],
22
26
  preambleMode: ['always', 'disabled'],
23
27
  };
@@ -26,7 +30,15 @@ function isValidKey(key: string): key is ConfigKey {
26
30
  return VALID_KEYS.includes(key as ConfigKey);
27
31
  }
28
32
 
29
- function isValidValue(key: ConfigKey, value: string): boolean {
33
+ function isEnumKey(key: ConfigKey): key is EnumConfigKey {
34
+ return ENUM_KEYS.includes(key as EnumConfigKey);
35
+ }
36
+
37
+ function isNumericKey(key: ConfigKey): key is NumericConfigKey {
38
+ return NUMERIC_KEYS.includes(key as NumericConfigKey);
39
+ }
40
+
41
+ function isValidValue(key: EnumConfigKey, value: string): boolean {
30
42
  return VALID_VALUES[key].includes(value);
31
43
  }
32
44
 
@@ -43,6 +55,7 @@ function showConfig(ctx: Context): void {
43
55
  resolved: {
44
56
  mode: ctx.config.mode,
45
57
  preambleMode: ctx.config.preambleMode,
58
+ preambleEvery: ctx.config.defaults.preambleEvery,
46
59
  defaults: ctx.config.defaults,
47
60
  },
48
61
  sources: {
@@ -52,6 +65,12 @@ function showConfig(ctx: Context): void {
52
65
  : globalConfig.preambleMode
53
66
  ? 'global'
54
67
  : 'default',
68
+ preambleEvery:
69
+ localSettings?.preambleEvery !== undefined
70
+ ? 'local'
71
+ : globalConfig.defaults?.preambleEvery !== undefined
72
+ ? 'global'
73
+ : 'default',
55
74
  },
56
75
  paths: {
57
76
  global: ctx.paths.globalConfig,
@@ -68,6 +87,12 @@ function showConfig(ctx: Context): void {
68
87
  : globalConfig.preambleMode
69
88
  ? '(global)'
70
89
  : '(default)';
90
+ const preambleEverySource =
91
+ localSettings?.preambleEvery !== undefined
92
+ ? '(local)'
93
+ : globalConfig.defaults?.preambleEvery !== undefined
94
+ ? '(global)'
95
+ : '(default)';
71
96
 
72
97
  ctx.ui.info('Current configuration:\n');
73
98
  ctx.ui.table(
@@ -75,6 +100,7 @@ function showConfig(ctx: Context): void {
75
100
  [
76
101
  ['mode', ctx.config.mode, modeSource],
77
102
  ['preambleMode', ctx.config.preambleMode, preambleSource],
103
+ ['preambleEvery', String(ctx.config.defaults.preambleEvery), preambleEverySource],
78
104
  ['defaults.timeout', String(ctx.config.defaults.timeout), '(global)'],
79
105
  ['defaults.pollInterval', String(ctx.config.defaults.pollInterval), '(global)'],
80
106
  ['defaults.captureLines', String(ctx.config.defaults.captureLines), '(global)'],
@@ -95,11 +121,25 @@ function setConfig(ctx: Context, key: string, value: string, global: boolean): v
95
121
  ctx.exit(ExitCodes.ERROR);
96
122
  }
97
123
 
98
- if (!isValidValue(key, value)) {
99
- ctx.ui.error(
100
- `Invalid value for ${key}: ${value}. Valid values: ${VALID_VALUES[key].join(', ')}`
101
- );
102
- ctx.exit(ExitCodes.ERROR);
124
+ const validKey = key as ConfigKey;
125
+
126
+ // Validate enum keys
127
+ if (isEnumKey(validKey)) {
128
+ if (!isValidValue(validKey, value)) {
129
+ ctx.ui.error(
130
+ `Invalid value for ${key}: ${value}. Valid values: ${VALID_VALUES[validKey].join(', ')}`
131
+ );
132
+ ctx.exit(ExitCodes.ERROR);
133
+ }
134
+ }
135
+
136
+ // Validate numeric keys
137
+ if (isNumericKey(validKey)) {
138
+ const numValue = parseInt(value, 10);
139
+ if (isNaN(numValue) || numValue < 0) {
140
+ ctx.ui.error(`Invalid value for ${key}: ${value}. Must be a non-negative integer.`);
141
+ ctx.exit(ExitCodes.ERROR);
142
+ }
103
143
  }
104
144
 
105
145
  if (global) {
@@ -109,6 +149,17 @@ function setConfig(ctx: Context, key: string, value: string, global: boolean): v
109
149
  globalConfig.mode = value as 'polling' | 'wait';
110
150
  } else if (key === 'preambleMode') {
111
151
  globalConfig.preambleMode = value as 'always' | 'disabled';
152
+ } else if (key === 'preambleEvery') {
153
+ if (!globalConfig.defaults) {
154
+ globalConfig.defaults = {
155
+ timeout: 180,
156
+ pollInterval: 1,
157
+ captureLines: 100,
158
+ preambleEvery: parseInt(value, 10),
159
+ };
160
+ } else {
161
+ globalConfig.defaults.preambleEvery = parseInt(value, 10);
162
+ }
112
163
  }
113
164
  saveGlobalConfig(ctx.paths, globalConfig);
114
165
  ctx.ui.success(`Set ${key}=${value} in global config`);
@@ -118,6 +169,8 @@ function setConfig(ctx: Context, key: string, value: string, global: boolean): v
118
169
  updateLocalSettings(ctx.paths, { mode: value as 'polling' | 'wait' });
119
170
  } else if (key === 'preambleMode') {
120
171
  updateLocalSettings(ctx.paths, { preambleMode: value as 'always' | 'disabled' });
172
+ } else if (key === 'preambleEvery') {
173
+ updateLocalSettings(ctx.paths, { preambleEvery: parseInt(value, 10) });
121
174
  }
122
175
  ctx.ui.success(`Set ${key}=${value} in local config (repo override)`);
123
176
  }
@@ -42,7 +42,6 @@ ${colors.yellow('COMMANDS')}
42
42
  ${colors.green('init')} Create empty tmux-team.json
43
43
  ${colors.green('config')} [show|set|clear] View/modify settings
44
44
  ${colors.green('preamble')} [show|set|clear] Manage agent preambles
45
- ${colors.green('pm')} <subcommand> Project management (run 'pm help')
46
45
  ${colors.green('completion')} Output shell completion script
47
46
  ${colors.green('help')} Show this help message
48
47
 
@@ -78,6 +77,7 @@ ${colors.yellow('CONFIG')}
78
77
  ${colors.yellow('CHANGE MODE')}
79
78
  tmux-team config set mode wait ${colors.dim('Enable wait mode (local)')}
80
79
  tmux-team config set mode polling ${colors.dim('Enable polling mode (local)')}
81
- tmux-team config set timeout 120 ${colors.dim('Set timeout to 120s')}
80
+ tmux-team config set preambleMode disabled ${colors.dim('Disable preambles (local)')}
81
+ tmux-team config set preambleEvery 5 ${colors.dim('Inject preamble every 5 messages')}
82
82
  `);
83
83
  }
@@ -1,10 +1,10 @@
1
1
  // ─────────────────────────────────────────────────────────────
2
- // Preamble command - manage agent preambles
2
+ // Preamble command - manage agent preambles (local config only)
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
5
  import type { Context } from '../types.js';
6
6
  import { ExitCodes } from '../context.js';
7
- import { loadGlobalConfig, saveGlobalConfig } from '../config.js';
7
+ import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
8
8
 
9
9
  /**
10
10
  * Show preamble(s) for agent(s).
@@ -57,25 +57,27 @@ function showPreamble(ctx: Context, agentName?: string): void {
57
57
  }
58
58
 
59
59
  /**
60
- * Set preamble for an agent.
60
+ * Set preamble for an agent (in local config).
61
61
  */
62
62
  function setPreamble(ctx: Context, agentName: string, preamble: string): void {
63
- const { ui, paths, flags } = ctx;
64
-
65
- const globalConfig = loadGlobalConfig(paths);
63
+ const { ui, paths, flags, config } = ctx;
66
64
 
67
- // Ensure agents object exists
68
- if (!globalConfig.agents) {
69
- globalConfig.agents = {};
65
+ // Check if agent exists in pane registry
66
+ if (!config.paneRegistry[agentName]) {
67
+ ui.error(`Agent '${agentName}' not found in local config`);
68
+ ui.error('Add the agent with: tmux-team add <agent> <pane>');
69
+ ctx.exit(ExitCodes.ERROR);
70
70
  }
71
71
 
72
- // Ensure agent config exists
73
- if (!globalConfig.agents[agentName]) {
74
- globalConfig.agents[agentName] = {};
72
+ const localConfig = loadLocalConfigFile(paths);
73
+
74
+ // Update preamble in local config
75
+ const agentEntry = localConfig[agentName] as { pane: string; preamble?: string } | undefined;
76
+ if (agentEntry) {
77
+ agentEntry.preamble = preamble;
75
78
  }
76
79
 
77
- globalConfig.agents[agentName].preamble = preamble;
78
- saveGlobalConfig(paths, globalConfig);
80
+ saveLocalConfigFile(paths, localConfig);
79
81
 
80
82
  if (flags.json) {
81
83
  ui.json({ agent: agentName, preamble, status: 'set' });
@@ -85,22 +87,17 @@ function setPreamble(ctx: Context, agentName: string, preamble: string): void {
85
87
  }
86
88
 
87
89
  /**
88
- * Clear preamble for an agent.
90
+ * Clear preamble for an agent (in local config).
89
91
  */
90
92
  function clearPreamble(ctx: Context, agentName: string): void {
91
93
  const { ui, paths, flags } = ctx;
92
94
 
93
- const globalConfig = loadGlobalConfig(paths);
94
-
95
- if (globalConfig.agents?.[agentName]?.preamble) {
96
- delete globalConfig.agents[agentName].preamble;
97
-
98
- // Clean up empty agent config
99
- if (Object.keys(globalConfig.agents[agentName]).length === 0) {
100
- delete globalConfig.agents[agentName];
101
- }
95
+ const localConfig = loadLocalConfigFile(paths);
96
+ const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
102
97
 
103
- saveGlobalConfig(paths, globalConfig);
98
+ if (agentEntry?.preamble) {
99
+ delete agentEntry.preamble;
100
+ saveLocalConfigFile(paths, localConfig);
104
101
 
105
102
  if (flags.json) {
106
103
  ui.json({ agent: agentName, status: 'cleared' });
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { Context } from '../types.js';
6
6
  import { ExitCodes } from '../exits.js';
7
- import { saveLocalConfig } from '../config.js';
7
+ import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
8
8
 
9
9
  export function cmdRemove(ctx: Context, name: string): void {
10
10
  const { ui, config, paths, flags, exit } = ctx;
@@ -14,8 +14,10 @@ export function cmdRemove(ctx: Context, name: string): void {
14
14
  exit(ExitCodes.PANE_NOT_FOUND);
15
15
  }
16
16
 
17
- delete config.paneRegistry[name];
18
- saveLocalConfig(paths, config.paneRegistry);
17
+ // Load existing config to preserve other agents' fields (preamble, deny, etc.)
18
+ const localConfig = loadLocalConfigFile(paths);
19
+ delete localConfig[name];
20
+ saveLocalConfigFile(paths, localConfig);
19
21
 
20
22
  if (flags.json) {
21
23
  ui.json({ removed: name });