Run script/setup in new worktrees and give each task a port block

This commit is contained in:
Ian Gulliver
2026-10-15 11:52:13 -07:00
parent f5e5dd30e3
commit cfa9699f4b
15 changed files with 453 additions and 174 deletions
+113
View File
@@ -0,0 +1,113 @@
package agents
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strconv"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/claude"
)
const (
setupScript = "script/setup"
setupTimeout = 15 * time.Minute
setupOutputBytes = 64 << 10
)
func portEnv(t api.Task) []string {
return []string{"PORT=" + strconv.Itoa(t.PortBase)}
}
func (m *Manager) startSetup(t api.Task) {
path := filepath.Join(t.WorktreePath, setupScript)
_, err := os.Stat(path)
if errors.Is(err, fs.ErrNotExist) {
return
}
m.setupMu.Lock()
m.setups[t.Id] = true
m.setupMu.Unlock()
go m.runSetup(t, path)
}
func (m *Manager) settingUp(taskID string) bool {
m.setupMu.Lock()
defer m.setupMu.Unlock()
return m.setups[taskID]
}
func (m *Manager) takeSetupFailure(taskID string) string {
m.setupMu.Lock()
defer m.setupMu.Unlock()
msg := m.setupFailures[taskID]
delete(m.setupFailures, taskID)
return msg
}
func (m *Manager) runSetup(t api.Task, path string) {
ctx := context.Background()
m.emitTask(ctx, t, "setup.started", map[string]any{"script": setupScript, "port_base": t.PortBase})
start := time.Now()
cctx, cancel := context.WithTimeout(ctx, setupTimeout)
defer cancel()
cmd := exec.CommandContext(cctx, path)
cmd.Dir = t.WorktreePath
cmd.Env = append(os.Environ(), portEnv(t)...)
out, err := cmd.CombinedOutput()
if cctx.Err() != nil {
err = fmt.Errorf("timed out after %s", setupTimeout)
}
output := string(out)
if len(out) > setupOutputBytes {
output = "…" + string(out[len(out)-setupOutputBytes:])
}
payload := map[string]any{"script": setupScript, "output": output, "duration_ms": time.Since(start).Milliseconds()}
if err != nil {
payload["error"] = err.Error()
}
slog.Info("setup ended", "task", t.Id, "err", err, "took", time.Since(start))
m.emitTask(ctx, t, "setup.ended", payload)
m.setupMu.Lock()
delete(m.setups, t.Id)
if err != nil {
m.setupFailures[t.Id] = fmt.Sprintf("byre ran %s in this worktree when the task was created, and it failed (%s). Its output:\n\n```\n%s\n```\n\nThe worktree may be missing dependencies or env files the setup would have provided.", setupScript, err, output)
}
m.setupMu.Unlock()
err = m.drain(ctx)
if err != nil {
slog.Error("start queued turns", "err", err)
}
}
func (m *Manager) emitTask(ctx context.Context, t api.Task, kind string, payload map[string]any) {
err := m.store.AppendEvent(ctx, api.Event{Kind: kind, ProjectId: &t.ProjectId, TaskId: &t.Id, Actor: "byre", Payload: payload})
if err != nil {
slog.Error("record setup event", "task", t.Id, "kind", kind, "err", err)
}
}
func (m *Manager) remindTaskGuidance(l *live, text string) claude.HookFunc {
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
failure := m.takeSetupFailure(l.taskID)
if failure == "" {
return remindGuidance(text)(ctx, input)
}
m.fileError(l, failure)
return remindGuidance(text+"\n\n"+failure)(ctx, input)
}
}