Bounce prose questions only when they ask the human to decide

This commit is contained in:
Ian Gulliver
2026-10-18 11:59:22 -07:00
parent aac99c48f9
commit 2658dcbfc4
6 changed files with 71 additions and 11 deletions
+10 -2
View File
@@ -259,8 +259,16 @@ func (m *Manager) codexTurnCompleted(ctx context.Context, l *live, s *codex.Sess
final := l.lastText
m.mu.Unlock()
if turn.Status == codex.TurnStatusCompleted && l.implementer() && m.takeBounce(l, final) {
err := m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true})
bounce := false
if turn.Status == codex.TurnStatusCompleted && l.implementer() {
var err error
bounce, err = m.takeBounce(ctx, l, final)
if err != nil {
return err
}
}
if bounce {
err := m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true, "bounce": true})
if err != nil {
return err
}
+39 -7
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"regexp"
"strings"
"unicode/utf8"
"g.fc.run/misc/byre/internal/api"
@@ -30,6 +31,9 @@ var (
inlineCode = regexp.MustCompile("`[^`\n]*`")
quoted = regexp.MustCompile(`"[^"\n]*"|“[^”\n]*”`)
blockquote = regexp.MustCompile(`(?m)^\s*>.*$`)
tableRow = regexp.MustCompile(`(?m)^\s*\|.*$`)
details = regexp.MustCompile(`(?s)<details>.*?</details>`)
paragraphs = regexp.MustCompile(`\n\s*\n`)
endsQuestion = regexp.MustCompile(`\?[*_)\]]*(\s|$)`)
)
@@ -38,7 +42,10 @@ func asksQuestion(text string) bool {
text = inlineCode.ReplaceAllString(text, "")
text = quoted.ReplaceAllString(text, "")
text = blockquote.ReplaceAllString(text, "")
return endsQuestion.MatchString(text)
text = tableRow.ReplaceAllString(text, "")
text = details.ReplaceAllString(text, "")
paras := paragraphs.Split(strings.TrimSpace(text), -1)
return endsQuestion.MatchString(paras[len(paras)-1])
}
func (m *Manager) bounceProseQuestion(l *live) claude.HookFunc {
@@ -51,21 +58,46 @@ func (m *Manager) bounceProseQuestion(l *live) claude.HookFunc {
return nil, err
}
if !m.takeBounce(l, in.LastAssistantMessage) {
return map[string]any{}, nil
bounce, err := m.takeBounce(ctx, l, in.LastAssistantMessage)
if err != nil || !bounce {
return map[string]any{}, err
}
err = m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true, "bounce": true})
if err != nil {
return nil, err
}
return map[string]any{"decision": "block", "reason": proseQuestionNudge}, nil
}
}
func (m *Manager) takeBounce(l *live, final string) bool {
func (m *Manager) takeBounce(ctx context.Context, l *live, final string) (bool, error) {
if !asksQuestion(final) {
return false, nil
}
first, err := m.store.FirstPrompt(ctx, l.taskID)
if err != nil {
return false, err
}
if strings.Contains(first, discussPrompt) {
return false, nil
}
open, err := m.store.ListInbox(ctx, store.InboxFilter{TaskID: l.taskID, State: api.Open})
if err != nil {
return false, err
}
for _, item := range open {
if item.Kind == api.Question {
return false, nil
}
}
m.mu.Lock()
defer m.mu.Unlock()
if l.asked || l.bounced || l.compacting || !asksQuestion(final) {
return false
if l.asked || l.bounced || l.compacting {
return false, nil
}
l.bounced = true
return true
return true, nil
}
func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, error) {
+3 -1
View File
@@ -88,9 +88,11 @@ func todoPrompt(item api.TodoItem, discuss bool) string {
if !discuss {
return p
}
return p + "\n\nThe human wants to discuss this item with you before any of it is built: a conversation, not a questionnaire. Read what you need to understand it, then end your turn with your thoughts on it in your final message: how you read it, how you would approach it, and what trade-offs or doubts you see. Don't use ask_human for this; the human replies in the chat. Keep the conversation going turn by turn, and change nothing until they tell you to build it."
return p + "\n\n" + discussPrompt
}
const discussPrompt = "The human wants to discuss this item with you before any of it is built: a conversation, not a questionnaire. Read what you need to understand it, then end your turn with your thoughts on it in your final message: how you read it, how you would approach it, and what trade-offs or doubts you see. Don't use ask_human for this; the human replies in the chat. Keep the conversation going turn by turn, and change nothing until they tell you to build it."
func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Task, frozen *store.FrozenWorkflow, base, actor string) (api.Task, error) {
t.Id = store.NewID()
t.ProjectId = project.Id