79 lines
3.1 KiB
Go
79 lines
3.1 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"g.fc.run/misc/byre/internal/api"
|
|
"g.fc.run/misc/byre/internal/claude"
|
|
"g.fc.run/misc/byre/internal/todo"
|
|
)
|
|
|
|
const guidance = `You are running inside byre. The human reads your work in byre's web UI, not a terminal.
|
|
- Ask every question with mcp__byre__ask_human, never in prose.
|
|
- End every turn with a short final message for the human: what you did, anything you departed from in the plan or instructions, risks, and what's next. byre shows it as the turn's summary in the transcript and the inbox. State every departure from the plan.
|
|
- In that final message and in ask_human context, put long supporting detail (logs, full lists, data) in collapsed sections the human expands on demand: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
|
|
- Don't commit. byre commits everything in the worktree at the end of each turn, using the turn's final message.
|
|
- Don't push, don't switch or touch other branches, and don't run git maintenance (gc, prune, worktree prune). byre handles merging and pushing.`
|
|
|
|
const proseQuestionNudge = "Your final message asks the human something in prose. Ask it with mcp__byre__ask_human " +
|
|
"(context with the choices inline as `- ( ) choice` / `- (*) recommended` or `- [ ] choice` / `- [x] recommended` list items, question, why_blocked), " +
|
|
"then end your turn with a short summary."
|
|
|
|
var (
|
|
fencedCode = regexp.MustCompile("(?s)```.*?```")
|
|
inlineCode = regexp.MustCompile("`[^`\n]*`")
|
|
quoted = regexp.MustCompile(`"[^"\n]*"|“[^”\n]*”`)
|
|
blockquote = regexp.MustCompile(`(?m)^\s*>.*$`)
|
|
endsQuestion = regexp.MustCompile(`\?[*_)\]]*(\s|$)`)
|
|
)
|
|
|
|
func asksQuestion(text string) bool {
|
|
text = fencedCode.ReplaceAllString(text, "")
|
|
text = inlineCode.ReplaceAllString(text, "")
|
|
text = quoted.ReplaceAllString(text, "")
|
|
text = blockquote.ReplaceAllString(text, "")
|
|
return endsQuestion.MatchString(text)
|
|
}
|
|
|
|
func (m *Manager) bounceProseQuestion(l *live) claude.HookFunc {
|
|
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
|
|
in := struct {
|
|
LastAssistantMessage string `json:"last_assistant_message"`
|
|
}{}
|
|
err := json.Unmarshal(input, &in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if l.asked || l.bounced || l.compacting || !asksQuestion(in.LastAssistantMessage) {
|
|
return map[string]any{}, nil
|
|
}
|
|
l.bounced = true
|
|
return map[string]any{"decision": "block", "reason": proseQuestionNudge}, nil
|
|
}
|
|
}
|
|
|
|
func taskGuidance(task api.Task) string {
|
|
if task.TodoItem == nil {
|
|
return guidance
|
|
}
|
|
return guidance + fmt.Sprintf("\n- This task was started from the item %q in %s at the repo root. When the work is done, remove that item from %s in this worktree, so the removal lands in the same merge as the work.",
|
|
*task.TodoItem, todo.File, todo.File)
|
|
}
|
|
|
|
func remindGuidance(text string) claude.HookFunc {
|
|
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
|
|
return map[string]any{
|
|
"hookSpecificOutput": map[string]any{
|
|
"hookEventName": "UserPromptSubmit",
|
|
"additionalContext": text,
|
|
},
|
|
}, nil
|
|
}
|
|
}
|