Thread optional grounding into the coach prompt

The ai.Coach interface gains a grounding parameter carrying standing
context about the user (the knowledge port). buildPrompt injects an
"About the user" block only when grounding is non-empty, so an empty
grounding produces a byte-for-byte unchanged prompt. Drift judge and
nudge are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 08:01:03 -04:00
parent 09061f8e30
commit 0f1790c8d5
2 changed files with 36 additions and 11 deletions
+13 -8
View File
@@ -2,9 +2,10 @@ package ai
import "context"
// Coach turns a free-text intent into a validated Proposal.
// Coach turns a free-text intent into a validated Proposal. grounding is
// optional standing context about the user (the knowledge port); "" means none.
type Coach interface {
Coach(ctx context.Context, intent string) (Proposal, error)
Coach(ctx context.Context, intent, grounding string) (Proposal, error)
}
// Backend is one way to reach an LLM CLI. Adapters differ only in the command
@@ -21,16 +22,16 @@ type Service struct {
func NewService(b Backend) *Service { return &Service{backend: b} }
func (s *Service) Coach(ctx context.Context, intent string) (Proposal, error) {
out, err := s.backend.Run(ctx, buildPrompt(intent))
func (s *Service) Coach(ctx context.Context, intent, grounding string) (Proposal, error) {
out, err := s.backend.Run(ctx, buildPrompt(intent, grounding))
if err != nil {
return Proposal{}, err
}
return parseProposal(out)
}
func buildPrompt(intent string) string {
return `You are a focus coach. The user gives a rough intent for a work session. Turn it into ONE concrete, actionable commitment.
func buildPrompt(intent, grounding string) string {
preamble := `You are a focus coach. The user gives a rough intent for a work session. Turn it into ONE concrete, actionable commitment.
Respond with ONLY a JSON object, no prose and no code fences, exactly this shape:
{"next_action": "<one concrete action to start now>", "success_condition": "<observable, verifiable done state>", "timebox_minutes": <integer, typically 15-50>, "allowed_window_classes": ["<app/window class that is on-task, e.g. code, firefox>"]}
@@ -39,9 +40,13 @@ Rules:
- next_action: a single concrete imperative action, doable now.
- success_condition: observable and verifiable; how you'd know it is done.
- timebox_minutes: a realistic integer number of minutes for the action.
- allowed_window_classes: a short list of window/application class names that count as on-task for this action (lowercase, e.g. "code", "firefox"). May be empty.
- allowed_window_classes: a short list of window/application class names that count as on-task for this action (lowercase, e.g. "code", "firefox"). May be empty.`
User intent: ` + intent
about := ""
if grounding != "" {
about = "\n\n## About the user\n" + grounding + "\nUse this standing context to make the commitment fit who they are and what matters to them."
}
return preamble + about + "\n\nUser intent: " + intent
}
// DriftJudge decides whether the current window is on-task for a commitment.