Run script/setup in new worktrees and give each task a port block
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user