Move questions and reviews into the transcript; agents answer findings

This commit is contained in:
Ian Gulliver
2026-10-19 09:14:21 -07:00
parent eb7e0a13c4
commit 27ff0f457b
21 changed files with 903 additions and 801 deletions
+3
View File
@@ -114,6 +114,9 @@ func codexToolCall(item codex.ThreadItem) (string, any, error) {
return "", nil, err
}
args, err := decode(item.Arguments)
if item.Type == codex.ThreadItemTypeDynamicToolCall && item.Namespace != "" {
tool = "mcp__" + item.Namespace + "__" + tool
}
return tool, args, err
}
+4 -7
View File
@@ -21,7 +21,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
- 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.
- Steering the right pane is a core part of your job, as important as what you write. The right pane shows one task, or a project with no task; point it with show (the task or project, and the tab to select). By default, keep it on whatever you and the human are discussing, so they can see it while they read your reply; don't make them go looking for it. Call show in the same turn you bring something up:
- Discussing, picking or starting TODO items, or a project's tasks as a whole: the project's project tab (show with project_id when no single task of that project is involved).
- A question that needs the human: that task's questions tab, with one line naming it and what you recommend.
- A question that needs the human: that task's transcript tab, where the question waits to be answered, with one line naming it and what you recommend.
- A finished turn that needs the human, discussing a task's work, what its agent did or is doing, or whether it is ready to merge: its transcript tab, which ends with the agent's final message. A specific file in its worktree: its files tab.
- The changes tab only when the human asks to see the diff; they rarely want it.
- Show, don't tell. What the right pane shows, the human reads there: never repeat, quote or paraphrase a task agent's question, choices, context or final message in chat. Add only what the pane doesn't say, such as your recommendation and why, or how it connects to other work.
@@ -29,12 +29,12 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
- Questions from task agents: answer one yourself with answer_question only when the answer is clear, for example the agent's recommendation is plainly right given what the human has said, the human already decided it, or one of your notes settles it outright. Otherwise show it to the human with your recommendation and wait. When the human tells you an answer, give it with answer_question. Say which questions you answered and how, naming the note by its id when one decided it.
- Notes: you learn how the human works and keep it with remember, forget and list_notes. byre adds every note to your instructions at the start of each lead session, so they outlast compaction; the human can read and delete them in the Settings tab. Write a note when the human shows a priority, a preference or the reasoning behind a choice that will recur: an answer that departs from the agent's recommendation (the answered signal shows both), a correction or overrule of you or a task agent, or a statement of how they want things done. Record the choice and the why, in their terms, general enough to apply to the next case and specific enough to act on, for example "Prefers replacing a state label over adding a tag beside it: keeps the picker uncluttered." File each note under the project it is about with project_id when it concerns that project's code, UI, docs or process, and with no project_id only when it holds for how the human works everywhere; apply a project's notes only to that project's tasks. Task agents start with their creator's every-project notes and their project's, and suggest new ones with suggest_note, which reaches you as a signal: save a suggestion only when it holds and your notes don't already cover it, choosing its project yourself, and otherwise drop it; neither needs a word to the human. Don't note one-off facts about a single task, and don't duplicate: update an existing note with remember and its id instead. When the human contradicts a note, update it or forget it. Before recommending anything, check your notes, and when one shaped the recommendation, cite its id.
- Finished turns: read them and show the human only what matters: work finished and ready to merge, problems, decisions made. Stay quiet about routine progress.
- Reviews: after each new snapshot of a task, a reviewer agent reviews the change, on the human's review model, or their second review model when the review model wrote the work. The results are on the task's reviews tab, and read_transcript shows each review's findings. They are for the human to look at when they want; mention one only when its findings matter to what the human is deciding.
- Reviews: after each new snapshot of a task, a reviewer agent reviews the change, on the human's review model, or their second review model when the review model wrote the work. The results are in the task's transcript, where its agent answers each finding with respond_to_finding (fixing, or ignoring with a reason), and read_transcript shows each review's findings. They are for the human to look at when they want; mention one only when its findings matter to what the human is deciding.
- Dig into context with list_tasks, read_transcript, read_inbox and read_status (vendor rate limits and agent health), and with your own Read, Grep and Bash on a task's worktree path (from list_tasks). Don't edit files in task worktrees; ask the task's agent with send_message.
- You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it.
- Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine.
- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. Every stage of a workflow runs as its own task on the stage's model, once the stages it comes after have finished, so stages fan out (several stages after one) and fan in (one stage after several); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and when the final stage finishes byre discards the run's other stage tasks, leaving the final one to merge. Whenever you show or propose a workflow, draw it as a Mermaid flowchart of its stage names only, with no models, for example a fenced mermaid block holding "flowchart LR", "Build --> Review", "Build --> Test", "Review --> Fix" and "Test --> Fix".
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/da7e487305e4). byre draws it in that task's colour, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer, link to #/task/<task_id>/questions; at a finished turn, to #/task/<task_id>/transcript.
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/da7e487305e4). byre draws it in that task's colour, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer or a finished turn, link to #/task/<task_id>/transcript.
- Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
- You are long-running. Your context window is large. byre sends you a signal once your context passes half of its window; then call compact_self at a logical point (a piece of work wrapped up, a topic closed), and not before; when the turn ends, byre replaces you with a fresh session that starts from your handoff.
- Don't commit, push, or touch git branches yourself.`
@@ -416,7 +416,7 @@ func (m *Manager) freedText(ctx context.Context, owner string, ev api.Event) (st
return m.waitingText(ctx, owner, ev)
}
switch ev.Kind {
case "inbox.updated", "task.merge_started", "task.merged", "task.discarded":
case "task.merge_started", "task.merged", "task.discarded":
default:
return "", nil
}
@@ -431,9 +431,6 @@ func (m *Manager) freedText(ctx context.Context, owner string, ev api.Event) (st
if err != nil || finished == "" {
return "", err
}
if ev.Kind == "inbox.updated" && finished != inboxEmptied {
return "", nil
}
return fmt.Sprintf("Your right pane is free: %s %s.", m.taskLabel(ctx, *f.TaskId), finished), nil
}
+4 -16
View File
@@ -457,17 +457,17 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
tool("show",
"Point the human's right pane at a task, or at a project with no task, and select one of its tabs. The tabs' order is fixed. "+
"Whenever the human asks you to show something (\"show me that when you can\") or you want to show something, call show right away; never hold back because you expect a refusal, since only a refused show arranges byre's signal to try again. "+
"Tabs: transcript, board (a team task's board: its agents' posts, the human's and yours), changes, files, terminal (a shell in the task's worktree), questions (the task's oldest open question), reviews (the task's reviews by reviewer agents), workflow (a workflow stage task's run: its graph and each stage's state, attempts and outputs), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+
"Tabs: transcript, board (a team task's board: its agents' posts, the human's and yours), changes, files, terminal (a shell in the task's worktree), workflow (a workflow stage task's run: its graph and each stage's state, attempts and outputs), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+
"Omit both task_id and project_id to clear the pane. "+
"If the pane already shows that task (or project) on that tab, nothing changes and you are told so. "+
"Otherwise refused for 10 seconds after the human last moved the pane themselves (switching task or tab), whatever else holds; if a show was refused for it, byre signals you \"The move hold is over\" when those 10 seconds pass without another move, and sends no signal otherwise. "+
"Otherwise refused while the human is working in the right pane (focused in it, or with unsent text in it), unless it is on the project tab, or what it shows is finished (a merged or discarded task, or a questions tab with nothing left open),"+
"Otherwise refused while the human is working in the right pane (focused in it, or with unsent text in it), unless it is on the project tab, or what it shows is finished (a merged or discarded task),"+
"or it is on the transcript tab of a task whose agent is mid-turn, the human has no unsent text there, and either the human sent the message that started that turn since the pane last moved or the pane moved there 10 seconds ago or more; byre signals you \"Your right pane is free\" when either happens. "+
"Also refused while the pane is on the project tab and the human started a TODO item there in the last 10 seconds, unless they switched task or tab or messaged you since; if a show was refused for it, byre signals you \"The start hold is over\" when those 10 seconds pass with the pane still on the project tab, and sends no signal otherwise.",
`{"type": "object", "properties": {
"task_id": {"type": "string"},
"project_id": {"type": "string", "description": "Ignored when task_id is given."},
"tab": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "questions", "reviews", "workflow", "project"]}
"tab": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "workflow", "project"]}
}, "required": ["tab"]}`,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
f := api.Focus{}
@@ -629,8 +629,6 @@ func sameView(a, b api.Focus) bool {
return deref(a.ProjectId) == deref(b.ProjectId)
}
const inboxEmptied = "has no open questions left"
func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error) {
if f.TaskId == nil {
return "", nil
@@ -645,17 +643,7 @@ func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error)
if task.Merging {
return "is merging", nil
}
if f.Tab != api.TabQuestions {
return "", nil
}
items, err := m.store.ListInbox(ctx, store.InboxFilter{State: api.Open, TaskID: task.Id})
if err != nil {
return "", err
}
if len(items) > 0 {
return "", nil
}
return inboxEmptied, nil
return "", nil
}
const watchHold = 10 * time.Second
+1 -1
View File
@@ -747,7 +747,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
return nil, err
}
l.notes = "\n\n" + taskNotesGuidance + "\n\n" + notes
tools = append(tools, m.suggestNoteTool(l), m.readyForMergeTool(l))
tools = append(tools, m.suggestNoteTool(l), m.readyForMergeTool(l), m.respondToFindingTool(l))
}
if sess.Role == api.Implementer && sess.Member != "" {
sessions, err := m.store.ListSessions(ctx, task.Id)
+68 -3
View File
@@ -364,9 +364,11 @@ func findingsMessage(r api.Review) string {
res = *r.Result
}
b := &strings.Builder{}
fmt.Fprintf(b, "A reviewer looked at snapshot #%d of your work and raised the findings below. Reviewers can be wrong or miss context: check each one against the code and your brief, act on the ones that hold up, and in your final message say briefly which you acted on and which you're leaving and why.", r.SnapshotN)
fmt.Fprintf(b, "A reviewer looked at snapshot #%d of your work and raised the findings below. Reviewers can be wrong or miss context: check each one against the code and your brief. "+
"Before you change anything, answer each finding with respond_to_finding (snapshot %d and the finding's number): fixing if it holds up and you'll act on it, ignoring with your reason if you're leaving it. "+
"The human sees your answers on the findings and can overrule an ignore.", r.SnapshotN, r.SnapshotN)
findings, _ := res["findings"].([]any)
for _, f := range findings {
for i, f := range findings {
fm, _ := f.(map[string]any)
where := ""
if p, ok := fm["path"].(string); ok && p != "" {
@@ -376,11 +378,74 @@ func findingsMessage(r api.Review) string {
}
where = " (`" + where + "`)"
}
fmt.Fprintf(b, "\n\n**[%v] %v**%s\n\n%s", fm["severity"], fm["title"], where, strings.TrimSpace(fmt.Sprint(fm["detail"])))
fmt.Fprintf(b, "\n\n**Finding %d: [%v] %v**%s\n\n%s", i+1, fm["severity"], fm["title"], where, strings.TrimSpace(fmt.Sprint(fm["detail"])))
}
return b.String()
}
type findingResponseArgs struct {
Snapshot int `json:"snapshot"`
Finding int `json:"finding"`
Response api.FindingResponseResponse `json:"response"`
Reason string `json:"reason"`
}
func (m *Manager) respondToFindingTool(l *live) mcp.Tool {
return mcp.Tool{
Name: "respond_to_finding",
Description: "Answer one finding of a review of your work, as soon as you've decided what to do about it: fixing when you'll act on it, ignoring when you're leaving it. " +
"The human sees your answer on the finding; an ignore keeps the human's fix button, so they can overrule you. Call it again to change your answer.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"snapshot": {"type": "integer", "description": "The reviewed snapshot's number."},
"finding": {"type": "integer", "description": "The finding's number in the review, from 1."},
"response": {"type": "string", "enum": ["fixing", "ignoring"]},
"reason": {"type": "string", "description": "One line: what you'll do about it, or why you're leaving it."}
},
"required": ["snapshot", "finding", "response", "reason"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
args := findingResponseArgs{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed respond_to_finding call: %w", err)
}
if !args.Response.Valid() {
return "", fmt.Errorf("%w: response must be fixing or ignoring", ErrInvalid)
}
if args.Finding < 1 {
return "", fmt.Errorf("%w: finding numbers start at 1", ErrInvalid)
}
reason := strings.TrimSpace(args.Reason)
if reason == "" {
return "", fmt.Errorf("%w: reason must not be empty", ErrInvalid)
}
reviews, err := m.store.ListReviews(ctx, l.taskID)
if err != nil {
return "", err
}
reviewID := ""
for _, r := range reviews {
if r.SnapshotN == args.Snapshot && r.State == api.ReviewStateDone {
reviewID = r.Id
}
}
if reviewID == "" {
return "", fmt.Errorf("%w: snapshot %d has no finished review", ErrInvalid, args.Snapshot)
}
err = m.store.SetFindingResponse(ctx, reviewID, args.Finding-1, api.FindingResponse{Response: args.Response, Reason: reason}, agentActor)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: the review of snapshot %d has no finding %d", ErrInvalid, args.Snapshot, args.Finding)
}
if err != nil {
return "", err
}
return fmt.Sprintf("Recorded: %s finding %d of snapshot %d's review.", args.Response, args.Finding, args.Snapshot), nil
},
}
}
type reviewImage struct {
entry map[string]any
contentType string