Extend coach proposal with allowed window classes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,12 +33,13 @@ 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.
|
return `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:
|
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>}
|
{"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>"]}
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- next_action: a single concrete imperative action, doable now.
|
- next_action: a single concrete imperative action, doable now.
|
||||||
- success_condition: observable and verifiable; how you'd know it is done.
|
- success_condition: observable and verifiable; how you'd know it is done.
|
||||||
- timebox_minutes: a realistic integer number of minutes for the action.
|
- 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.
|
||||||
|
|
||||||
User intent: ` + intent
|
User intent: ` + intent
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -16,6 +16,7 @@ type Proposal struct {
|
|||||||
NextAction string
|
NextAction string
|
||||||
SuccessCondition string
|
SuccessCondition string
|
||||||
TimeboxSecs int64
|
TimeboxSecs int64
|
||||||
|
AllowedWindowClasses []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -64,6 +65,7 @@ type rawProposal struct {
|
|||||||
NextAction string `json:"next_action"`
|
NextAction string `json:"next_action"`
|
||||||
SuccessCondition string `json:"success_condition"`
|
SuccessCondition string `json:"success_condition"`
|
||||||
TimeboxMinutes int64 `json:"timebox_minutes"`
|
TimeboxMinutes int64 `json:"timebox_minutes"`
|
||||||
|
AllowedWindowClasses []string `json:"allowed_window_classes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseProposal extracts and validates a Proposal from raw CLI output.
|
// parseProposal extracts and validates a Proposal from raw CLI output.
|
||||||
@@ -91,5 +93,16 @@ func parseProposal(s string) (Proposal, error) {
|
|||||||
if na == "" || sc == "" || raw.TimeboxMinutes <= 0 {
|
if na == "" || sc == "" || raw.TimeboxMinutes <= 0 {
|
||||||
return Proposal{}, ErrInvalidProposal
|
return Proposal{}, ErrInvalidProposal
|
||||||
}
|
}
|
||||||
return Proposal{NextAction: na, SuccessCondition: sc, TimeboxSecs: raw.TimeboxMinutes * 60}, nil
|
classes := make([]string, 0, len(raw.AllowedWindowClasses))
|
||||||
|
for _, cls := range raw.AllowedWindowClasses {
|
||||||
|
if t := strings.TrimSpace(cls); t != "" {
|
||||||
|
classes = append(classes, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Proposal{
|
||||||
|
NextAction: na,
|
||||||
|
SuccessCondition: sc,
|
||||||
|
TimeboxSecs: raw.TimeboxMinutes * 60,
|
||||||
|
AllowedWindowClasses: classes,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,3 +69,23 @@ func TestParseProposal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseProposalReadsAllowedClasses(t *testing.T) {
|
||||||
|
p, err := parseProposal(`{"next_action":"a","success_condition":"b","timebox_minutes":20,"allowed_window_classes":["code"," firefox ",""]}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
if len(p.AllowedWindowClasses) != 2 || p.AllowedWindowClasses[0] != "code" || p.AllowedWindowClasses[1] != "firefox" {
|
||||||
|
t.Fatalf("classes wrong (blanks should drop, trim applied): %#v", p.AllowedWindowClasses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseProposalAllowedClassesOptional(t *testing.T) {
|
||||||
|
p, err := parseProposal(`{"next_action":"a","success_condition":"b","timebox_minutes":20}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
if len(p.AllowedWindowClasses) != 0 {
|
||||||
|
t.Fatalf("absent field should yield empty slice, got %#v", p.AllowedWindowClasses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user