Skip to content

Commit 7b81a4a

Browse files
committed
refactor(.agents): improve tmux session management with JSON output and robust parsing
- tmux-start.sh now outputs JSON: {status:"success",sessionName:"..."} - Add proper JSON escaping for special chars (backslash, quote, newline, tab, cr) - tmux-cli.sh parses JSON internally, returns plain session name for callers - Simplify CLI agent handleSteps to parse plain text instead of JSON - Add TmuxStartResult type for the JSON contract - Fix stdout/stderr consistency: errors now go to stderr in both modes - Remove conflicting manual tmux start instructions from system prompt - Add skipPrepPhase config flag for agents that start sessions immediately
1 parent b254e98 commit 7b81a4a

File tree

8 files changed

+194
-162
lines changed

8 files changed

+194
-162
lines changed

.agents/claude-code-cli.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,38 +42,26 @@ const definition: AgentDefinition = {
4242
},
4343
}
4444

45+
// Parse response from tmux-cli.sh (outputs plain session name on success, error to stderr on failure)
4546
let sessionName = ''
4647
let parseError = ''
4748

48-
if (!toolResult || toolResult.length === 0) {
49-
parseError = 'No result returned from run_terminal_command'
50-
} else {
51-
const result = toolResult[0]
52-
if (!result || result.type !== 'json') {
53-
logger.warn({ resultType: result?.type }, 'Unexpected toolResult type (expected json)')
54-
parseError = 'Unexpected result type: ' + (result?.type ?? 'undefined')
55-
} else {
56-
const value = result.value
57-
if (typeof value === 'string') {
58-
sessionName = value.trim()
59-
} else if (value && typeof value === 'object') {
60-
const obj = value as Record<string, unknown>
61-
const exitCode = typeof obj.exitCode === 'number' ? obj.exitCode : undefined
62-
const stderr = typeof obj.stderr === 'string' ? obj.stderr : ''
63-
const stdout = typeof obj.stdout === 'string' ? obj.stdout : ''
49+
const result = toolResult?.[0]
50+
if (result && result.type === 'json') {
51+
const value = result.value as Record<string, unknown>
52+
const stdout = typeof value?.stdout === 'string' ? value.stdout.trim() : ''
53+
const stderr = typeof value?.stderr === 'string' ? value.stderr.trim() : ''
54+
const exitCode = typeof value?.exitCode === 'number' ? value.exitCode : undefined
6455

65-
if (exitCode !== undefined && exitCode !== 0) {
66-
logger.error({ exitCode, stderr }, 'tmux-cli.sh start failed with non-zero exit code')
67-
parseError = 'Command failed with exit code ' + exitCode + (stderr ? ': ' + stderr : '')
68-
} else {
69-
const output = typeof obj.output === 'string' ? obj.output : ''
70-
sessionName = (stdout || output).trim()
71-
}
72-
} else {
73-
logger.warn({ valueType: typeof value }, 'Unexpected toolResult value format')
74-
parseError = 'Unexpected value format: ' + typeof value
75-
}
56+
if (!stdout && !stderr) {
57+
parseError = 'tmux-cli.sh returned empty output'
58+
} else if (exitCode !== 0 || !stdout) {
59+
parseError = stderr || 'tmux-cli.sh failed with no error message'
60+
} else {
61+
sessionName = stdout
7662
}
63+
} else {
64+
parseError = 'Unexpected result type from run_terminal_command'
7765
}
7866

7967
if (!sessionName) {

.agents/codebuff-local-cli.ts

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const baseDefinition = createCliAgent({
1111
permissionNote:
1212
'No permission flags needed for Codebuff local dev server.',
1313
model: 'anthropic/claude-opus-4.5',
14+
skipPrepPhase: true,
1415
spawnerPromptExtras: `**Use this agent after modifying:**
1516
- \`cli/src/components/\` - UI components, layouts, rendering
1617
- \`cli/src/hooks/\` - hooks that affect what users see
@@ -20,7 +21,6 @@ const baseDefinition = createCliAgent({
2021
})
2122

2223
// Constants must be inside handleSteps since it gets serialized via .toString()
23-
// No prep phase needed since this tests Codebuff itself, not an external tool
2424
const definition: AgentDefinition = {
2525
...baseDefinition,
2626
handleSteps: function* ({ prompt, params, logger }) {
@@ -37,38 +37,26 @@ const definition: AgentDefinition = {
3737
},
3838
}
3939

40+
// Parse response from tmux-cli.sh (outputs plain session name on success, error to stderr on failure)
4041
let sessionName = ''
4142
let parseError = ''
4243

43-
if (!toolResult || toolResult.length === 0) {
44-
parseError = 'No result returned from run_terminal_command'
45-
} else {
46-
const result = toolResult[0]
47-
if (!result || result.type !== 'json') {
48-
logger.warn({ resultType: result?.type }, 'Unexpected toolResult type (expected json)')
49-
parseError = 'Unexpected result type: ' + (result?.type ?? 'undefined')
50-
} else {
51-
const value = result.value
52-
if (typeof value === 'string') {
53-
sessionName = value.trim()
54-
} else if (value && typeof value === 'object') {
55-
const obj = value as Record<string, unknown>
56-
const exitCode = typeof obj.exitCode === 'number' ? obj.exitCode : undefined
57-
const stderr = typeof obj.stderr === 'string' ? obj.stderr : ''
58-
const stdout = typeof obj.stdout === 'string' ? obj.stdout : ''
44+
const result = toolResult?.[0]
45+
if (result && result.type === 'json') {
46+
const value = result.value as Record<string, unknown>
47+
const stdout = typeof value?.stdout === 'string' ? value.stdout.trim() : ''
48+
const stderr = typeof value?.stderr === 'string' ? value.stderr.trim() : ''
49+
const exitCode = typeof value?.exitCode === 'number' ? value.exitCode : undefined
5950

60-
if (exitCode !== undefined && exitCode !== 0) {
61-
logger.error({ exitCode, stderr }, 'tmux-cli.sh start failed with non-zero exit code')
62-
parseError = 'Command failed with exit code ' + exitCode + (stderr ? ': ' + stderr : '')
63-
} else {
64-
const output = typeof obj.output === 'string' ? obj.output : ''
65-
sessionName = (stdout || output).trim()
66-
}
67-
} else {
68-
logger.warn({ valueType: typeof value }, 'Unexpected toolResult value format')
69-
parseError = 'Unexpected value format: ' + typeof value
70-
}
51+
if (!stdout && !stderr) {
52+
parseError = 'tmux-cli.sh returned empty output'
53+
} else if (exitCode !== 0 || !stdout) {
54+
parseError = stderr || 'tmux-cli.sh failed with no error message'
55+
} else {
56+
sessionName = stdout
7157
}
58+
} else {
59+
parseError = 'Unexpected result type from run_terminal_command'
7260
}
7361

7462
if (!sessionName) {

.agents/codex-cli.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -122,38 +122,26 @@ const definition: AgentDefinition = {
122122
},
123123
}
124124

125+
// Parse response from tmux-cli.sh (outputs plain session name on success, error to stderr on failure)
125126
let sessionName = ''
126127
let parseError = ''
127128

128-
if (!toolResult || toolResult.length === 0) {
129-
parseError = 'No result returned from run_terminal_command'
130-
} else {
131-
const result = toolResult[0]
132-
if (!result || result.type !== 'json') {
133-
logger.warn({ resultType: result?.type }, 'Unexpected toolResult type (expected json)')
134-
parseError = 'Unexpected result type: ' + (result?.type ?? 'undefined')
129+
const result = toolResult?.[0]
130+
if (result && result.type === 'json') {
131+
const value = result.value as Record<string, unknown>
132+
const stdout = typeof value?.stdout === 'string' ? value.stdout.trim() : ''
133+
const stderr = typeof value?.stderr === 'string' ? value.stderr.trim() : ''
134+
const exitCode = typeof value?.exitCode === 'number' ? value.exitCode : undefined
135+
136+
if (!stdout && !stderr) {
137+
parseError = 'tmux-cli.sh returned empty output'
138+
} else if (exitCode !== 0 || !stdout) {
139+
parseError = stderr || 'tmux-cli.sh failed with no error message'
135140
} else {
136-
const value = result.value
137-
if (typeof value === 'string') {
138-
sessionName = value.trim()
139-
} else if (value && typeof value === 'object') {
140-
const obj = value as Record<string, unknown>
141-
const exitCode = typeof obj.exitCode === 'number' ? obj.exitCode : undefined
142-
const stderr = typeof obj.stderr === 'string' ? obj.stderr : ''
143-
const stdout = typeof obj.stdout === 'string' ? obj.stdout : ''
144-
145-
if (exitCode !== undefined && exitCode !== 0) {
146-
logger.error({ exitCode, stderr }, 'tmux-cli.sh start failed with non-zero exit code')
147-
parseError = 'Command failed with exit code ' + exitCode + (stderr ? ': ' + stderr : '')
148-
} else {
149-
const output = typeof obj.output === 'string' ? obj.output : ''
150-
sessionName = (stdout || output).trim()
151-
}
152-
} else {
153-
logger.warn({ valueType: typeof value }, 'Unexpected toolResult value format')
154-
parseError = 'Unexpected value format: ' + typeof value
155-
}
141+
sessionName = stdout
156142
}
143+
} else {
144+
parseError = 'Unexpected result type from run_terminal_command'
157145
}
158146

159147
if (!sessionName) {

.agents/gemini-cli.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,38 +48,26 @@ const definition: AgentDefinition = {
4848
},
4949
}
5050

51+
// Parse response from tmux-cli.sh (outputs plain session name on success, error to stderr on failure)
5152
let sessionName = ''
5253
let parseError = ''
5354

54-
if (!toolResult || toolResult.length === 0) {
55-
parseError = 'No result returned from run_terminal_command'
56-
} else {
57-
const result = toolResult[0]
58-
if (!result || result.type !== 'json') {
59-
logger.warn({ resultType: result?.type }, 'Unexpected toolResult type (expected json)')
60-
parseError = 'Unexpected result type: ' + (result?.type ?? 'undefined')
55+
const result = toolResult?.[0]
56+
if (result && result.type === 'json') {
57+
const value = result.value as Record<string, unknown>
58+
const stdout = typeof value?.stdout === 'string' ? value.stdout.trim() : ''
59+
const stderr = typeof value?.stderr === 'string' ? value.stderr.trim() : ''
60+
const exitCode = typeof value?.exitCode === 'number' ? value.exitCode : undefined
61+
62+
if (!stdout && !stderr) {
63+
parseError = 'tmux-cli.sh returned empty output'
64+
} else if (exitCode !== 0 || !stdout) {
65+
parseError = stderr || 'tmux-cli.sh failed with no error message'
6166
} else {
62-
const value = result.value
63-
if (typeof value === 'string') {
64-
sessionName = value.trim()
65-
} else if (value && typeof value === 'object') {
66-
const obj = value as Record<string, unknown>
67-
const exitCode = typeof obj.exitCode === 'number' ? obj.exitCode : undefined
68-
const stderr = typeof obj.stderr === 'string' ? obj.stderr : ''
69-
const stdout = typeof obj.stdout === 'string' ? obj.stdout : ''
70-
71-
if (exitCode !== undefined && exitCode !== 0) {
72-
logger.error({ exitCode, stderr }, 'tmux-cli.sh start failed with non-zero exit code')
73-
parseError = 'Command failed with exit code ' + exitCode + (stderr ? ': ' + stderr : '')
74-
} else {
75-
const output = typeof obj.output === 'string' ? obj.output : ''
76-
sessionName = (stdout || output).trim()
77-
}
78-
} else {
79-
logger.warn({ valueType: typeof value }, 'Unexpected toolResult value format')
80-
parseError = 'Unexpected value format: ' + typeof value
81-
}
67+
sessionName = stdout
8268
}
69+
} else {
70+
parseError = 'Unexpected result type from run_terminal_command'
8371
}
8472

8573
if (!sessionName) {

.agents/lib/cli-agent-prompts.ts

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -102,64 +102,44 @@ export function getSystemPrompt(config: CliAgentConfig): string {
102102

103103
return `You are an expert at using ${config.cliName} CLI via tmux for implementation work and code reviews. You have access to helper scripts that handle the complexities of tmux communication with TUI apps.
104104
105-
## ${config.cliName} Startup
105+
## Session Management
106106
107-
To start ${config.cliName}, use the \`--command\` flag with permission bypass:
107+
**A tmux session is started for you automatically.** The session name will be announced in an assistant message. Use that session name (stored in \`$SESSION\`) for all subsequent commands.
108108
109-
\`\`\`bash
110-
# Start ${config.cliName} CLI (with permission bypass)
111-
SESSION=$(./scripts/tmux/tmux-cli.sh start --command "${config.startCommand}")
112-
113-
# Or with specific options
114-
SESSION=$(./scripts/tmux/tmux-cli.sh start --command "${config.startCommand} --help")
115-
\`\`\`
109+
**Do NOT start a new session** - use the one that was started for you.
116110
117111
**Important:** ${config.permissionNote}
118112
${cliSpecificSection}
119113
## Helper Scripts
120114
121-
Use these scripts in \`scripts/tmux/\` for reliable CLI interaction:
122-
123-
### Unified Script (Recommended)
115+
Use these scripts in \`scripts/tmux/\` to interact with the CLI session:
124116
125117
\`\`\`bash
126-
# Start a ${config.cliName} session (with permission bypass)
127-
SESSION=$(./scripts/tmux/tmux-cli.sh start --command "${config.startCommand}")
128-
129118
# Send input to the CLI
130119
./scripts/tmux/tmux-cli.sh send "$SESSION" "/help"
131120
132121
# Capture output (optionally wait first)
133122
./scripts/tmux/tmux-cli.sh capture "$SESSION" --wait 3
134123
124+
# Capture with a descriptive label
125+
./scripts/tmux/tmux-cli.sh capture "$SESSION" --label "after-task" --wait 5
126+
135127
# Stop the session when done
136128
./scripts/tmux/tmux-cli.sh stop "$SESSION"
137-
138-
# Stop all test sessions
139-
./scripts/tmux/tmux-cli.sh stop --all
140129
\`\`\`
141130
142-
### Individual Scripts (More Options)
131+
### Additional Options
143132
144133
\`\`\`bash
145-
# Start with custom settings
146-
./scripts/tmux/tmux-start.sh --command "${config.startCommand}" --name ${config.shortName}-test --width 160 --height 40
147-
148-
# Send text (auto-presses Enter)
149-
./scripts/tmux/tmux-send.sh ${config.shortName}-test "your prompt here"
150-
151134
# Send without pressing Enter
152-
./scripts/tmux/tmux-send.sh ${config.shortName}-test "partial" --no-enter
135+
./scripts/tmux/tmux-send.sh "$SESSION" "partial" --no-enter
153136
154137
# Send special keys
155-
./scripts/tmux/tmux-send.sh ${config.shortName}-test --key Escape
156-
./scripts/tmux/tmux-send.sh ${config.shortName}-test --key C-c
138+
./scripts/tmux/tmux-send.sh "$SESSION" --key Escape
139+
./scripts/tmux/tmux-send.sh "$SESSION" --key C-c
157140
158141
# Capture with colors
159-
./scripts/tmux/tmux-capture.sh ${config.shortName}-test --colors
160-
161-
# Save capture to file
162-
./scripts/tmux/tmux-capture.sh ${config.shortName}-test -o output.txt
142+
./scripts/tmux/tmux-capture.sh "$SESSION" --colors
163143
\`\`\`
164144
165145
## Why These Scripts?
@@ -291,9 +271,11 @@ export function getInstructionsPrompt(config: CliAgentConfig): string {
291271
const nonDefaultModes = CLI_AGENT_MODES.filter(m => m !== defaultMode)
292272
const modeChecks = nonDefaultModes.map(m => `- If \`mode\` is "${m}": follow **${modeNames[m]}** instructions`).join('\n')
293273

294-
return `Instructions:
274+
const workflowSection = config.skipPrepPhase
275+
? `## Workflow
295276
296-
## Two-Phase Workflow
277+
**A tmux session is started for you immediately.** An assistant message will announce the session name. **Do NOT start a new session** - use the one provided.`
278+
: `## Two-Phase Workflow
297279
298280
This agent operates in two phases:
299281
@@ -307,7 +289,11 @@ You have an opportunity to prepare before the CLI session starts. Use this time
307289
After your preparation turn, a tmux session will be started automatically.
308290
309291
### Phase 2: CLI Execution
310-
Once the session starts, an assistant message will announce the session name. **Do NOT start a new session** - use the one provided.
292+
Once the session starts, an assistant message will announce the session name. **Do NOT start a new session** - use the one provided.`
293+
294+
return `Instructions:
295+
296+
${workflowSection}
311297
312298
Check the \`mode\` parameter to determine your operation:
313299
${modeChecks}

.agents/lib/cli-agent-types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
export type CliAgentMode = 'work' | 'review'
22

3+
/**
4+
* Result type for tmux-start.sh JSON output.
5+
* The shell script outputs this JSON format to stdout.
6+
* See: scripts/tmux/tmux-start.sh
7+
*/
8+
export type TmuxStartResult =
9+
| { status: 'success'; sessionName: string }
10+
| { status: 'failure'; error: string }
11+
312
export const CLI_AGENT_MODES: readonly CliAgentMode[] = ['work', 'review'] as const
413

514
export interface InputParamDefinition {
@@ -34,4 +43,10 @@ export interface CliAgentConfig {
3443
/** Custom instructions for review mode. If not provided, uses getDefaultReviewModeInstructions() */
3544
reviewModeInstructions?: string
3645
cliSpecificDocs?: string
46+
/**
47+
* If true, skips the preparation phase before starting the tmux session.
48+
* Use this for agents that test the CLI itself (like codebuff-local-cli)
49+
* rather than external tools that need context gathering.
50+
*/
51+
skipPrepPhase?: boolean
3752
}

scripts/tmux/tmux-cli.sh

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,35 @@ shift
103103

104104
case "$COMMAND" in
105105
start)
106-
exec "$SCRIPT_DIR/tmux-start.sh" "$@"
106+
# Run tmux-start.sh and parse its JSON output
107+
# This gives callers a plain session name for backward compatibility
108+
JSON_OUTPUT=$("$SCRIPT_DIR/tmux-start.sh" "$@" 2>&1) || true
109+
110+
# Check if output looks like JSON
111+
if [[ "$JSON_OUTPUT" == "{"* ]]; then
112+
# Parse JSON to extract session name or error
113+
# Use grep/sed for portability (no jq dependency)
114+
if echo "$JSON_OUTPUT" | grep -q '"status":"success"'; then
115+
# Extract sessionName value
116+
SESSION_NAME=$(echo "$JSON_OUTPUT" | sed -n 's/.*"sessionName":"\([^"]*\)".*/\1/p')
117+
if [[ -n "$SESSION_NAME" ]]; then
118+
echo "$SESSION_NAME"
119+
exit 0
120+
else
121+
echo "Failed to extract session name from: $JSON_OUTPUT" >&2
122+
exit 1
123+
fi
124+
else
125+
# Extract error message
126+
ERROR_MSG=$(echo "$JSON_OUTPUT" | sed -n 's/.*"error":"\([^"]*\)".*/\1/p')
127+
echo "${ERROR_MSG:-Failed to start session}" >&2
128+
exit 1
129+
fi
130+
else
131+
# Not JSON - pass through as-is (plain mode or unexpected output)
132+
echo "$JSON_OUTPUT"
133+
exit 0
134+
fi
107135
;;
108136
send)
109137
exec "$SCRIPT_DIR/tmux-send.sh" "$@"

0 commit comments

Comments
 (0)