Skip to content

Commit b254e98

Browse files
committed
feat(.agents): add programmatic handleSteps to CLI agents for enforced tmux invocation
- Add two-phase workflow (prep → CLI execution) for external CLI agents - codebuff-local-cli skips prep phase (tests Codebuff itself) - Add 30s timeout and standardized session name parsing - Make sessionName required in output schema - Update prompts to explain two-phase workflow
1 parent 18e72ca commit b254e98

File tree

7 files changed

+483
-38
lines changed

7 files changed

+483
-38
lines changed

.agents/claude-code-cli.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createCliAgent } from './lib/create-cli-agent'
22

3-
export default createCliAgent({
3+
import type { AgentDefinition } from './types/agent-definition'
4+
5+
const baseDefinition = createCliAgent({
46
id: 'claude-code-cli',
57
displayName: 'Claude Code CLI',
68
cliName: 'Claude Code',
@@ -10,3 +12,111 @@ export default createCliAgent({
1012
'Always use `--dangerously-skip-permissions` when testing to avoid permission prompts that would block automated tests.',
1113
model: 'anthropic/claude-opus-4.5',
1214
})
15+
16+
// Constants must be inside handleSteps since it gets serialized via .toString()
17+
const definition: AgentDefinition = {
18+
...baseDefinition,
19+
handleSteps: function* ({ prompt, params, logger }) {
20+
const START_COMMAND = 'claude --dangerously-skip-permissions'
21+
const CLI_NAME = 'Claude Code'
22+
23+
yield {
24+
toolName: 'add_message',
25+
input: {
26+
role: 'assistant',
27+
content: 'I\'ll first gather context and prepare before starting the ' + CLI_NAME + ' CLI session.\n\n' +
28+
'Let me read relevant files and understand the task to provide better guidance to the CLI.',
29+
},
30+
includeToolCall: false,
31+
}
32+
33+
yield 'STEP'
34+
35+
logger.info('Starting ' + CLI_NAME + ' tmux session...')
36+
37+
const { toolResult } = yield {
38+
toolName: 'run_terminal_command',
39+
input: {
40+
command: './scripts/tmux/tmux-cli.sh start --command "' + START_COMMAND + '"',
41+
timeout_seconds: 30,
42+
},
43+
}
44+
45+
let sessionName = ''
46+
let parseError = ''
47+
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 : ''
64+
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+
}
76+
}
77+
}
78+
79+
if (!sessionName) {
80+
const errorMsg = parseError || 'Session name was empty'
81+
logger.error({ parseError: errorMsg }, 'Failed to start tmux session')
82+
yield {
83+
toolName: 'set_output',
84+
input: {
85+
overallStatus: 'failure',
86+
summary: 'Failed to start ' + CLI_NAME + ' tmux session. ' + errorMsg,
87+
sessionName: '',
88+
scriptIssues: [
89+
{
90+
script: 'tmux-cli.sh',
91+
issue: errorMsg,
92+
errorOutput: JSON.stringify(toolResult),
93+
suggestedFix: 'Ensure tmux-cli.sh outputs the session name to stdout and exits with code 0. Check that tmux is installed.',
94+
},
95+
],
96+
captures: [],
97+
},
98+
}
99+
return
100+
}
101+
102+
logger.info('Successfully started tmux session: ' + sessionName)
103+
104+
yield {
105+
toolName: 'add_message',
106+
input: {
107+
role: 'assistant',
108+
content: 'I have started a ' + CLI_NAME + ' tmux session: `' + sessionName + '`\n\n' +
109+
'I will use this session for all CLI interactions. The session name must be included in my final output.\n\n' +
110+
'Now I\'ll proceed with the task using the helper scripts:\n' +
111+
'- Send commands: `./scripts/tmux/tmux-cli.sh send "' + sessionName + '" "..."`\n' +
112+
'- Capture output: `./scripts/tmux/tmux-cli.sh capture "' + sessionName + '" --label "..."`\n' +
113+
'- Stop when done: `./scripts/tmux/tmux-cli.sh stop "' + sessionName + '"`',
114+
},
115+
includeToolCall: false,
116+
}
117+
118+
yield 'STEP_ALL'
119+
},
120+
}
121+
122+
export default definition

.agents/codebuff-local-cli.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createCliAgent } from './lib/create-cli-agent'
22

3-
export default createCliAgent({
3+
import type { AgentDefinition } from './types/agent-definition'
4+
5+
const baseDefinition = createCliAgent({
46
id: 'codebuff-local-cli',
57
displayName: 'Codebuff Local CLI',
68
cliName: 'Codebuff',
@@ -16,3 +18,100 @@ export default createCliAgent({
1618
1719
**When to use:** After implementing CLI UI changes, use this to verify the visual output actually renders correctly. Unit tests and typechecks cannot catch layout bugs, rendering issues, or visual regressions. This agent captures real terminal output including colors and layout.`,
1820
})
21+
22+
// 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
24+
const definition: AgentDefinition = {
25+
...baseDefinition,
26+
handleSteps: function* ({ prompt, params, logger }) {
27+
const START_COMMAND = 'bun --cwd=cli run dev'
28+
const CLI_NAME = 'Codebuff'
29+
30+
logger.info('Starting ' + CLI_NAME + ' tmux session...')
31+
32+
const { toolResult } = yield {
33+
toolName: 'run_terminal_command',
34+
input: {
35+
command: './scripts/tmux/tmux-cli.sh start --command "' + START_COMMAND + '"',
36+
timeout_seconds: 30,
37+
},
38+
}
39+
40+
let sessionName = ''
41+
let parseError = ''
42+
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 : ''
59+
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+
}
71+
}
72+
}
73+
74+
if (!sessionName) {
75+
const errorMsg = parseError || 'Session name was empty'
76+
logger.error({ parseError: errorMsg }, 'Failed to start tmux session')
77+
yield {
78+
toolName: 'set_output',
79+
input: {
80+
overallStatus: 'failure',
81+
summary: 'Failed to start ' + CLI_NAME + ' tmux session. ' + errorMsg,
82+
sessionName: '',
83+
scriptIssues: [
84+
{
85+
script: 'tmux-cli.sh',
86+
issue: errorMsg,
87+
errorOutput: JSON.stringify(toolResult),
88+
suggestedFix: 'Ensure tmux-cli.sh outputs the session name to stdout and exits with code 0. Check that tmux is installed.',
89+
},
90+
],
91+
captures: [],
92+
},
93+
}
94+
return
95+
}
96+
97+
logger.info('Successfully started tmux session: ' + sessionName)
98+
99+
yield {
100+
toolName: 'add_message',
101+
input: {
102+
role: 'assistant',
103+
content: 'I have started a ' + CLI_NAME + ' tmux session: `' + sessionName + '`\n\n' +
104+
'I will use this session for all CLI interactions. The session name must be included in my final output.\n\n' +
105+
'Now I\'ll proceed with the task using the helper scripts:\n' +
106+
'- Send commands: `./scripts/tmux/tmux-cli.sh send "' + sessionName + '" "..."`\n' +
107+
'- Capture output: `./scripts/tmux/tmux-cli.sh capture "' + sessionName + '" --label "..."`\n' +
108+
'- Stop when done: `./scripts/tmux/tmux-cli.sh stop "' + sessionName + '"`',
109+
},
110+
includeToolCall: false,
111+
}
112+
113+
yield 'STEP_ALL'
114+
},
115+
}
116+
117+
export default definition

0 commit comments

Comments
 (0)