Bold questions

This commit is contained in:
Ian Gulliver
2026-10-14 19:35:34 -07:00
parent ab4c3d2c6f
commit 1ea055682b
54 changed files with 3696 additions and 762 deletions
+132 -43
View File
@@ -22,12 +22,11 @@ import (
)
type Server struct {
store *store.Store
agents *agents.Manager
hub *hub
worktreeRoot string
startedAt time.Time
spa fs.FS
store *store.Store
agents *agents.Manager
hub *hub
startedAt time.Time
spa fs.FS
usersMu sync.Mutex
users map[string]string
@@ -42,11 +41,10 @@ func New(dataDir string, spa fs.FS) (*Server, error) {
}
s := &Server{
hub: newHub(),
worktreeRoot: filepath.Join(dataDir, "worktrees"),
startedAt: time.Now().UTC(),
spa: spa,
users: map[string]string{},
hub: newHub(),
startedAt: time.Now().UTC(),
spa: spa,
users: map[string]string{},
}
s.store, err = store.Open(filepath.Join(dataDir, "byre.db"), s.hub.publish)
@@ -54,7 +52,7 @@ func New(dataDir string, spa fs.FS) (*Server, error) {
return nil, err
}
s.agents = agents.New(s.store, agents.DefaultLimits)
s.agents = agents.New(s.store, agents.DefaultLimits, filepath.Join(dataDir, "worktrees"))
err = s.agents.Recover(context.Background())
if err != nil {
s.store.Close()
@@ -245,25 +243,7 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st
return
}
id := store.NewID()
t := api.Task{
Id: id,
ProjectId: p.Id,
Title: req.Title,
Branch: "byre/" + id,
WorktreePath: filepath.Join(s.worktreeRoot, p.Id, id),
Status: api.TaskStatusActive,
CreatedBy: actor(r),
CreatedAt: time.Now().UTC(),
}
t.BaseRef, err = git.AddWorktree(p.RepoPath, t.WorktreePath, t.Branch, p.DefaultBranch)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
err = s.store.CreateTask(r.Context(), t)
t, err := s.agents.CreateTask(r.Context(), p, req.Title, actor(r), p.DefaultBranch)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
@@ -280,37 +260,75 @@ func (s *Server) GetTask(w http.ResponseWriter, r *http.Request, taskID string)
writeJSON(w, http.StatusOK, t)
}
func (s *Server) CompleteTask(w http.ResponseWriter, r *http.Request, taskID string) {
t, err := s.store.GetTask(r.Context(), taskID)
func (s *Server) GetUnmerged(w http.ResponseWriter, r *http.Request, taskID string) {
t, p, ok := s.activeTask(w, r, taskID)
if !ok {
return
}
u, err := git.UnmergedChanges(t.WorktreePath, p.DefaultBranch)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, api.Unmerged{
Commits: u.Commits,
Files: u.Files,
Insertions: u.Insertions,
Deletions: u.Deletions,
Uncommitted: u.Uncommitted,
})
}
func (s *Server) MergeTask(w http.ResponseWriter, r *http.Request, taskID string) {
t, p, ok := s.activeTask(w, r, taskID)
if !ok {
return
}
u, err := s.store.GetUser(r.Context(), actor(r))
if err != nil {
writeStoreError(w, err)
return
}
if t.Status != api.TaskStatusActive {
writeError(w, http.StatusConflict, fmt.Errorf("task %s is %s", t.Id, t.Status))
if u.GitName == "" || u.GitEmail == "" {
writeError(w, http.StatusBadRequest, errors.New("set your git name and email (PUT /api/me) before merging"))
return
}
p, err := s.store.GetProject(r.Context(), t.ProjectId)
sessions, err := s.store.ListSessions(r.Context(), t.Id)
if err != nil {
writeStoreError(w, err)
return
}
for _, sess := range sessions {
if sess.State == api.AgentSessionStateRunning || sess.State == api.AgentSessionStateQueued {
writeError(w, http.StatusConflict, fmt.Errorf("session %s is %s; wait for the turn to end or interrupt it", sess.Id, sess.State))
return
}
}
err = s.agents.StopTask(r.Context(), t.Id, actor(r))
commit, err := git.SquashMerge(p.RepoPath, t.WorktreePath, p.DefaultBranch, squashMessage(t), []string{
"GIT_AUTHOR_NAME=" + u.GitName,
"GIT_AUTHOR_EMAIL=" + u.GitEmail,
"GIT_COMMITTER_NAME=" + u.GitName,
"GIT_COMMITTER_EMAIL=" + u.GitEmail,
})
conflict := &git.ConflictError{}
if errors.As(err, &conflict) {
writeError(w, http.StatusConflict, err)
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
err = git.RemoveWorktree(p.RepoPath, t.WorktreePath, t.Branch)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
if !s.removeTask(w, r, t, p) {
return
}
t, err = s.store.CompleteTask(r.Context(), taskID, actor(r))
t, err = s.store.MergeTask(r.Context(), taskID, commit, actor(r))
if err != nil {
writeStoreError(w, err)
return
@@ -318,6 +336,63 @@ func (s *Server) CompleteTask(w http.ResponseWriter, r *http.Request, taskID str
writeJSON(w, http.StatusOK, t)
}
func (s *Server) DiscardTask(w http.ResponseWriter, r *http.Request, taskID string) {
t, p, ok := s.activeTask(w, r, taskID)
if !ok {
return
}
if !s.removeTask(w, r, t, p) {
return
}
t, err := s.store.DiscardTask(r.Context(), taskID, actor(r))
if err != nil {
writeStoreError(w, err)
return
}
writeJSON(w, http.StatusOK, t)
}
func squashMessage(t api.Task) string {
return t.Title
}
func (s *Server) activeTask(w http.ResponseWriter, r *http.Request, taskID string) (api.Task, api.Project, bool) {
t, err := s.store.GetTask(r.Context(), taskID)
if err != nil {
writeStoreError(w, err)
return t, api.Project{}, false
}
if t.Status != api.TaskStatusActive {
writeError(w, http.StatusConflict, fmt.Errorf("task %s is %s", t.Id, t.Status))
return t, api.Project{}, false
}
p, err := s.store.GetProject(r.Context(), t.ProjectId)
if err != nil {
writeStoreError(w, err)
return t, p, false
}
return t, p, true
}
func (s *Server) removeTask(w http.ResponseWriter, r *http.Request, t api.Task, p api.Project) bool {
err := s.agents.StopTask(r.Context(), t.Id, actor(r))
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return false
}
err = git.RemoveWorktree(p.RepoPath, t.WorktreePath, t.Branch)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return false
}
return true
}
func (s *Server) GetStatus(w http.ResponseWriter, r *http.Request) {
c, err := s.store.Counts(r.Context())
if err != nil {
@@ -448,7 +523,21 @@ func (s *Server) SendMessage(w http.ResponseWriter, r *http.Request, sessionID s
return
}
sess, err := s.agents.Send(r.Context(), sessionID, actor(r), req.Text)
model := ""
if req.Model != nil {
model = *req.Model
}
sess, err := s.agents.Send(r.Context(), sessionID, actor(r), req.Text, model)
if err != nil {
writeAgentError(w, err)
return
}
writeJSON(w, http.StatusAccepted, sess)
}
func (s *Server) CompactSession(w http.ResponseWriter, r *http.Request, sessionID string) {
sess, err := s.agents.Compact(r.Context(), sessionID)
if err != nil {
writeAgentError(w, err)
return