lead agent auto merge

This commit is contained in:
Ian Gulliver
2026-10-18 23:41:53 -07:00
parent d567261fbd
commit 01486ea7fa
3 changed files with 30 additions and 4 deletions
+2 -2
View File
File diff suppressed because one or more lines are too long
+14 -2
View File
@@ -15,7 +15,7 @@ import (
) )
const leadGuidance = `You are the human's lead agent in byre, shown in the left pane of byre's app. The human talks to you there, and you coordinate every task agent on their behalf. You don't do task work yourself: task agents work in their own worktrees, and you read, direct and report on them. const leadGuidance = `You are the human's lead agent in byre, shown in the left pane of byre's app. The human talks to you there, and you coordinate every task agent on their behalf. You don't do task work yourself: task agents work in their own worktrees, and you read, direct and report on them.
- Signals: byre sends you, as messages from byre, what happens across tasks: questions that task agents ask, errors and rate limits their sessions hit, turns they finish with their final message, questions the human answered, tasks created, reviews started and finished, starting to merge, merged or discarded, and every change of a task's state, as "<task> state: <before> → <now>." listed first among what happened at the same moment. The states are running, idle, review_pending, reviewing, merging, discarding, merged, discarded and no_agent. A finished turn arrives with its state change: review_pending when byre is about to review the turn's work, reviewing while the review runs; neither is done, since the review can still find problems, so don't treat a task's work as finished until its state goes to idle. Several may arrive in one message, followed by the current task list (as list_tasks gives it) and what your right pane shows, so you rarely need to call list_tasks after a signal. The human doesn't see signals. Messages from the human arrive separately. - Signals: byre sends you, as messages from byre, what happens across tasks: questions that task agents ask, errors and rate limits their sessions hit, turns they finish with their final message, questions the human answered, tasks created, reviews started and finished, a merge held behind another task's or set to start after the review (and that cancelled or dropped), starting to merge, merged or discarded, and every change of a task's state, as "<task> state: <before> → <now>." listed first among what happened at the same moment. The states are running, idle, review_pending, reviewing, merging, discarding, merged, discarded and no_agent. A finished turn arrives with its state change: review_pending when byre is about to review the turn's work, reviewing while the review runs; neither is done, since the review can still find problems, so don't treat a task's work as finished until its state goes to idle. Several may arrive in one message, followed by the current task list (as list_tasks gives it) and what your right pane shows, so you rarely need to call list_tasks after a signal. The human doesn't see signals. Messages from the human arrive separately.
- Don't respond to what the human just did. A signal about their own action (a question they answered, a task they created, merged, started merging or discarded) tells them nothing new: don't report, confirm or comment on it. Act on it only if it changes what you are doing, and then end the turn with one short line at most. When a batch of signals needs nothing from you to the human, make your final message exactly NO_REPLY and nothing else; byre folds that turn away so the human doesn't see it. - Don't respond to what the human just did. A signal about their own action (a question they answered, a task they created, merged, started merging or discarded) tells them nothing new: don't report, confirm or comment on it. Act on it only if it changes what you are doing, and then end the turn with one short line at most. When a batch of signals needs nothing from you to the human, make your final message exactly NO_REPLY and nothing else; byre folds that turn away so the human doesn't see it.
- "Your right pane is free" means the human finished what was in the right pane. Show them the next thing that needs them, most urgent first (questions, then errors on the task's transcript tab, then finished turns, then tasks ready to merge), with one line saying what it is. If nothing needs them, show that project's project tab, then post a short status: one line per active task (what it's doing or waiting on), then up to three TODO items you'd start next (from list_tasks), each with a one-line reason. Write each item as a link to #/todo/<project_id>/<title, URL-encoded with parentheses as %28 and %29>, for example [Fix login redirect](#/todo/3f2a9c1b7d4e/Fix%20login%20redirect): byre draws it with a Start button the human can click, and once the task exists, its state. Start one with start_todo only when the human picks it. If that says nothing new since your last status, make your final message NO_REPLY. - "Your right pane is free" means the human finished what was in the right pane. Show them the next thing that needs them, most urgent first (questions, then errors on the task's transcript tab, then finished turns, then tasks ready to merge), with one line saying what it is. If nothing needs them, show that project's project tab, then post a short status: one line per active task (what it's doing or waiting on), then up to three TODO items you'd start next (from list_tasks), each with a one-line reason. Write each item as a link to #/todo/<project_id>/<title, URL-encoded with parentheses as %28 and %29>, for example [Fix login redirect](#/todo/3f2a9c1b7d4e/Fix%20login%20redirect): byre draws it with a Start button the human can click, and once the task exists, its state. Start one with start_todo only when the human picks it. If that says nothing new since your last status, make your final message NO_REPLY.
- Whenever you suggest new work that isn't in TODO.md (a task you think is worth doing, an improvement to byre or to your own role), write it as a link to #/suggest/<project_id>/<title>/<brief>, both URL-encoded with parentheses as %28 and %29, for example [Lead tool for admin status](#/suggest/3f2a9c1b7d4e/Lead%20tool%20for%20admin%20status/Add%20a%20lead%20tool%20that%20reads%20%2Fapi%2Fadmin%2Fstatus%20...). The brief is the new task agent's whole first message, so make it a complete brief, as for create_task. byre draws the link with two buttons: Start creates the task with that title and brief at once, without you, and Discuss sends you a message asking to talk the suggestion over. Once an active task has that title, it shows the task's state instead. Don't ask whether to start a suggestion; the buttons do that. - Whenever you suggest new work that isn't in TODO.md (a task you think is worth doing, an improvement to byre or to your own role), write it as a link to #/suggest/<project_id>/<title>/<brief>, both URL-encoded with parentheses as %28 and %29, for example [Lead tool for admin status](#/suggest/3f2a9c1b7d4e/Lead%20tool%20for%20admin%20status/Add%20a%20lead%20tool%20that%20reads%20%2Fapi%2Fadmin%2Fstatus%20...). The brief is the new task agent's whole first message, so make it a complete brief, as for create_task. byre draws the link with two buttons: Start creates the task with that title and brief at once, without you, and Discuss sends you a message asking to talk the suggestion over. Once an active task has that title, it shows the task's state instead. Don't ask whether to start a suggestion; the buttons do that.
@@ -321,7 +321,7 @@ func (m *Manager) Observe(events []api.Event) {
batch := []api.Event{} batch := []api.Event{}
for _, ev := range events { for _, ev := range events {
switch ev.Kind { switch ev.Kind {
case "inbox.created", "inbox.updated", "task.created", "task.merge_started", "task.merged", "task.discarded", "turn.ended", "task.workflow_stage", "task.workflow_finished", "review.started", "review.finished", "task.state": case "inbox.created", "inbox.updated", "task.created", "task.merge_queued", "task.merge_after_review", "task.merge_after_review_cancelled", "task.merge_after_review_dropped", "task.merge_started", "task.merged", "task.discarded", "turn.ended", "task.workflow_stage", "task.workflow_finished", "review.started", "review.finished", "task.state":
case "user.message": case "user.message":
if ev.TaskId == nil || !humanActor(ev.Actor) { if ev.TaskId == nil || !humanActor(ev.Actor) {
continue continue
@@ -500,6 +500,18 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error)
} }
return fmt.Sprintf("%s was created by %s from TODO item %q.", m.taskLabel(ctx, task.Id), ev.Actor, *task.TodoItem), nil return fmt.Sprintf("%s was created by %s from TODO item %q.", m.taskLabel(ctx, task.Id), ev.Actor, *task.TodoItem), nil
case "task.merge_queued":
return fmt.Sprintf("%s's merge was requested by %s and is held until the merge in progress in its project is done.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
case "task.merge_after_review":
return fmt.Sprintf("%s was set by %s to merge once the review of snapshot %v finishes with no findings.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor, ev.Payload["snapshot_n"]), nil
case "task.merge_after_review_cancelled":
return fmt.Sprintf("%s's merge after review was cancelled by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
case "task.merge_after_review_dropped":
return fmt.Sprintf("%s didn't merge after the review: %v", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["reason"]), nil
case "task.merge_started": case "task.merge_started":
return fmt.Sprintf("%s is merging, started by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil return fmt.Sprintf("%s is merging, started by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
+14
View File
@@ -896,6 +896,12 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) {
b.WriteString(", merging into it") b.WriteString(", merging into it")
} }
} }
if t.MergeQueuedAt != nil {
b.WriteString("; merge held until the merge in progress in its project is done")
}
if t.MergeAfterReview {
b.WriteString("; merges once the running review finishes with no findings")
}
if t.Workflow != nil { if t.Workflow != nil {
stage, err := m.workflowStatus(ctx, t) stage, err := m.workflowStatus(ctx, t)
if err != nil { if err != nil {
@@ -1034,6 +1040,14 @@ func transcriptLine(ev api.Event, root string) string {
if p["error"] != nil { if p["error"] != nil {
return fmt.Sprintf("session %s: %s", text("state"), text("error")) return fmt.Sprintf("session %s: %s", text("state"), text("error"))
} }
case "task.merge_queued":
return fmt.Sprintf("merge held, requested by %s", ev.Actor)
case "task.merge_after_review":
return fmt.Sprintf("set by %s to merge after the review of snapshot %s", ev.Actor, text("snapshot_n"))
case "task.merge_after_review_cancelled":
return fmt.Sprintf("merge after review cancelled by %s", ev.Actor)
case "task.merge_after_review_dropped":
return "didn't merge after the review: " + clip(text("reason"), 300)
case "task.merged", "task.discarded": case "task.merged", "task.discarded":
return fmt.Sprintf("%s by %s", ev.Kind, ev.Actor) return fmt.Sprintf("%s by %s", ev.Kind, ev.Actor)
case "task.workflow_stage": case "task.workflow_stage":