Move-hold signal only after refused show

This commit is contained in:
Ian Gulliver
2026-10-17 09:24:13 -07:00
parent 248c0d5904
commit d1dda3f8ab
4 changed files with 38 additions and 13 deletions
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -25,7 +25,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
- 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.
- Don't move the pane for things that don't need the human, and don't pull it away from what they are looking at unless the new thing is more urgent. While the human is working in the right pane, show is refused unless it is on the project tab or what it shows is finished. It is also refused on the project tab for 10 seconds after the human starts a TODO item there, since they are likely to start more. It is always refused for 10 seconds after the human moves the pane themselves, since they just picked what to look at. When show is refused, say in chat what you would have shown, and carry on. "The start hold is over" means those 10 seconds passed with the pane still on the project tab, and "The move hold is over" means 10 seconds passed since the human's last move: show what you held back, if it still matters and the pane's other rules allow it, or make your final message NO_REPLY.
- Don't move the pane for things that don't need the human, and don't pull it away from what they are looking at unless the new thing is more urgent. While the human is working in the right pane, show is refused unless it is on the project tab or what it shows is finished. It is also refused on the project tab for 10 seconds after the human starts a TODO item there, since they are likely to start more. It is always refused for 10 seconds after the human moves the pane themselves, since they just picked what to look at. Whenever the human asks you to show something (for example "show me that when you can") or you want to show something, call show right away and never hold back expecting a refusal: a refused show is the only thing that arranges byre's signal to try again, so a show you skip is never retried. When show is refused, say in chat what you would have shown, and carry on. "The start hold is over" means those 10 seconds passed with the pane still on the project tab, and "The move hold is over" means 10 seconds passed since the human's last move; byre sends either only when a show of yours was refused during that hold. Then show what you held back, if it still matters and the pane's other rules allow it, or make your final message NO_REPLY.
- 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.
+31 -10
View File
@@ -407,13 +407,14 @@ 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: tabs are the tabs to put first, in order (the rest follow), and tab is the one to select. "+
"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, changes, files, terminal (a shell in the task's worktree), inbox (the task's open items), 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. "+
"Omit both task_id and project_id to clear the pane. "+
"If the pane already shows that task (or project) on that tab, only the tab order changes and you are told so, even while the human is working in the pane. "+
"Otherwise refused for 10 seconds after the human last moved the pane themselves (switching task or tab), whatever else holds; byre signals you \"The move hold is over\" when those 10 seconds pass without another move. "+
"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, unless it is on the project tab, or what it shows is finished (a merged or discarded task, or an inbox tab with nothing left), "+
"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; byre signals you \"The start hold is over\" when those 10 seconds pass with the pane still on the project tab.",
"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."},
@@ -443,13 +444,21 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
m.mu.Lock()
focused := m.paneFocus[user.Login].Focused
humanMoved := m.humanMoved[user.Login]
startedTodo := m.startedTodo[user.Login]
m.mu.Unlock()
if since := time.Since(humanMoved); since < moveHold {
return "", fmt.Errorf("%w: the human moved the right pane %s ago, so it was left as it is; tell them in chat instead. The hold ends %s after their last move; byre signals you \"The move hold is over\" when it ends", ErrConflict, since.Round(time.Second), moveHold)
since := time.Since(humanMoved)
moveHeld := since < moveHold
startHeld := !moveHeld && time.Since(m.startedTodo[user.Login]) < startHold && current.Tab == api.TabProject
if moveHeld {
m.moveRefused[user.Login] = true
}
if time.Since(startedTodo) < startHold && current.Tab == api.TabProject {
return "", fmt.Errorf("%w: the human just started a TODO item from the project tab and is likely to start more; tell them in chat instead. The hold ends %s after that start, unless they move the pane or message you first; byre signals you \"The start hold is over\" when it ends", ErrConflict, startHold)
if startHeld {
m.startRefused[user.Login] = true
}
m.mu.Unlock()
if moveHeld {
return "", fmt.Errorf("%w: the human moved the right pane %s ago, so it was left as it is; tell them in chat instead. The hold ends %s after their last move; byre signals you \"The move hold is over\" when it ends, because this show was refused", ErrConflict, since.Round(time.Second), moveHold)
}
if startHeld {
return "", fmt.Errorf("%w: the human just started a TODO item from the project tab and is likely to start more; tell them in chat instead. The hold ends %s after that start, unless they move the pane or message you first; byre signals you \"The start hold is over\" when it ends, because this show was refused", ErrConflict, startHold)
}
if focused && current.Tab != api.TabProject {
finished, err := m.paneFinished(ctx, current)
@@ -518,6 +527,7 @@ func (m *Manager) SetFocus(ctx context.Context, login string, f api.Focus, actor
m.paneMoved[login] = at
if actor == login {
m.humanMoved[login] = at
delete(m.moveRefused, login)
}
m.mu.Unlock()
if actor == login {
@@ -640,7 +650,7 @@ func (m *Manager) SetPaneFocus(login string, p api.PaneFocus) {
const moveHold = 10 * time.Second
const moveHoldEnded = "The move hold is over: the human moved the right pane 10 seconds ago and hasn't moved it since, so show works again under its other rules."
const moveHoldEnded = "The move hold is over: the human moved the right pane 10 seconds ago and hasn't moved it since, and a show of yours was refused in that time, so show works again under its other rules."
func (m *Manager) endMoveHold(ctx context.Context, login string, at time.Time) error {
m.mu.Lock()
@@ -650,7 +660,12 @@ func (m *Manager) endMoveHold(ctx context.Context, login string, at time.Time) e
return nil
}
delete(m.humanMoved, login)
refused := m.moveRefused[login]
delete(m.moveRefused, login)
m.mu.Unlock()
if !refused {
return nil
}
err := m.store.AddLeadInput(ctx, store.LeadInput{Owner: login, Kind: store.LeadSignal, Actor: "byre", Text: moveHoldEnded})
if err != nil {
return err
@@ -661,11 +676,12 @@ func (m *Manager) endMoveHold(ctx context.Context, login string, at time.Time) e
const startHold = 10 * time.Second
const startHoldEnded = "The start hold is over: the human started a TODO item from the project tab 10 seconds ago and the pane is still there, so show works again."
const startHoldEnded = "The start hold is over: the human started a TODO item from the project tab 10 seconds ago and the pane is still there, and a show of yours was refused in that time, so show works again."
func (m *Manager) setStartedTodo(login string, started bool) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.startRefused, login)
if !started {
delete(m.startedTodo, login)
return
@@ -688,7 +704,12 @@ func (m *Manager) endStartHold(ctx context.Context, login string, at time.Time)
return nil
}
delete(m.startedTodo, login)
refused := m.startRefused[login]
delete(m.startRefused, login)
m.mu.Unlock()
if !refused {
return nil
}
f, err := m.store.GetFocus(ctx, login)
if err != nil {
return err
+4
View File
@@ -50,7 +50,9 @@ type Manager struct {
paneReported map[string]time.Time
paneMoved map[string]time.Time
humanMoved map[string]time.Time
moveRefused map[string]bool
startedTodo map[string]time.Time
startRefused map[string]bool
finishing map[string]bool
deliverMu sync.Mutex
@@ -132,7 +134,9 @@ func New(st *store.Store, worktreeRoot, leadRoot, codexHome string, status func(
paneReported: map[string]time.Time{},
paneMoved: map[string]time.Time{},
humanMoved: map[string]time.Time{},
moveRefused: map[string]bool{},
startedTodo: map[string]time.Time{},
startRefused: map[string]bool{},
finishing: map[string]bool{},
signals: make(chan []api.Event, 1024),
snaps: map[string]*snapState{},