feat: setup and cleanup script (#80)

fix setup

error messages

feat: codex error handling
This commit is contained in:
Carlos Florêncio
2026-01-23 23:36:04 +00:00
committed by GitHub
parent 8426bcb6bb
commit cff2906712
23 changed files with 1508 additions and 36 deletions
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os/exec"
"regexp"
"strings"
"sync"
"sync/atomic"
@@ -58,6 +59,9 @@ type PermissionNotification struct {
CreatedAt time.Time `json:"created_at"`
}
// defaultStderrBufferSize is the number of recent stderr lines to keep for error context
const defaultStderrBufferSize = 50
// Manager manages the agent subprocess
type Manager struct {
cfg *config.InstanceConfig
@@ -72,6 +76,10 @@ type Manager struct {
exitCode atomic.Int32
exitErr atomic.Value // error
// Stderr buffering for error context
stderrBuffer []string
stderrMu sync.RWMutex
// Workspace tracker for git status and file changes
workspaceTracker *WorkspaceTracker
@@ -386,7 +394,10 @@ func (m *Manager) createAdapter() error {
case agent.ProtocolACP:
m.adapter = adapter.NewACPAdapter(m.adapterCfg, m.logger)
case agent.ProtocolCodex:
m.adapter = adapter.NewCodexAdapter(m.adapterCfg, m.logger)
codexAdapter := adapter.NewCodexAdapter(m.adapterCfg, m.logger)
// Set stderr provider so errors can include recent stderr output
codexAdapter.SetStderrProvider(m)
m.adapter = codexAdapter
default:
return fmt.Errorf("unsupported protocol: %s", protocol)
}
@@ -528,6 +539,9 @@ func (m *Manager) readStderr() {
for scanner.Scan() {
line := scanner.Text()
m.logger.Debug("agent stderr", zap.String("line", line))
// Buffer the line for error context
m.appendStderr(line)
}
if err := scanner.Err(); err != nil {
@@ -535,6 +549,46 @@ func (m *Manager) readStderr() {
}
}
// ansiEscapeRegex matches ANSI escape sequences
var ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
// stripANSI removes ANSI escape codes from a string
func stripANSI(s string) string {
return ansiEscapeRegex.ReplaceAllString(s, "")
}
// appendStderr adds a line to the stderr ring buffer
func (m *Manager) appendStderr(line string) {
m.stderrMu.Lock()
defer m.stderrMu.Unlock()
// Strip ANSI escape codes for cleaner display
cleanLine := stripANSI(line)
if len(m.stderrBuffer) >= defaultStderrBufferSize {
// Ring buffer: drop oldest line
m.stderrBuffer = m.stderrBuffer[1:]
}
m.stderrBuffer = append(m.stderrBuffer, cleanLine)
}
// GetRecentStderr returns a copy of the recent stderr lines
func (m *Manager) GetRecentStderr() []string {
m.stderrMu.RLock()
defer m.stderrMu.RUnlock()
result := make([]string, len(m.stderrBuffer))
copy(result, m.stderrBuffer)
return result
}
// ClearStderrBuffer clears the stderr buffer (e.g., after successful operation)
func (m *Manager) ClearStderrBuffer() {
m.stderrMu.Lock()
defer m.stderrMu.Unlock()
m.stderrBuffer = nil
}
// waitForExit waits for the process to exit
func (m *Manager) waitForExit() {
defer m.wg.Done()
@@ -76,3 +76,76 @@ func TestProcessRunnerCapturesOutput(t *testing.T) {
}
t.Fatal("process output not captured in time")
}
func TestStripANSI(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "no ANSI codes",
input: "hello world",
expected: "hello world",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "simple color code",
input: "\x1b[31mred text\x1b[0m",
expected: "red text",
},
{
name: "bold text",
input: "\x1b[1mbold\x1b[0m",
expected: "bold",
},
{
name: "multiple colors",
input: "\x1b[31mred\x1b[32mgreen\x1b[34mblue\x1b[0m",
expected: "redgreenblue",
},
{
name: "256 color code",
input: "\x1b[38;5;196mcolored\x1b[0m",
expected: "colored",
},
{
name: "RGB color code",
input: "\x1b[38;2;255;0;0mred\x1b[0m",
expected: "red",
},
{
name: "cursor movement",
input: "\x1b[2Amove up\x1b[3Bmove down",
expected: "move upmove down",
},
{
name: "clear line",
input: "text\x1b[2Kcleared",
expected: "textcleared",
},
{
name: "real world npm output",
input: "\x1b[32m✓\x1b[39m \x1b[90mCompiled successfully\x1b[39m",
expected: "✓ Compiled successfully",
},
{
name: "mixed with newlines",
input: "\x1b[31mError:\x1b[0m\nSomething failed\n\x1b[33mWarning:\x1b[0m check logs",
expected: "Error:\nSomething failed\nWarning: check logs",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := stripANSI(tt.input)
if result != tt.expected {
t.Errorf("stripANSI(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}