blob: c663ebb66f8ca0f7657aadd260a45034083a0c7a [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package loop
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7
Earl Lee2e463fb2025-04-17 11:22:22 -07008 "sketch.dev/claudetool"
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -07009 "sketch.dev/llm"
Earl Lee2e463fb2025-04-17 11:22:22 -070010)
11
12// makeDoneTool creates a tool that provides a checklist to the agent. There
13// are some duplicative instructions here and in the system prompt, and it's
14// not as reliable as it could be. Historically, we've found that Claude ignores
15// the tool results here, so we don't tell the tool to say "hey, really check this"
16// at the moment, though we've tried.
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070017func makeDoneTool(codereview *claudetool.CodeReviewer, gitUsername, gitEmail string) *llm.Tool {
18 return &llm.Tool{
Earl Lee2e463fb2025-04-17 11:22:22 -070019 Name: "done",
Josh Bleecher Snyderd7e56382025-05-05 13:22:08 -070020 Description: doneDescription,
Earl Lee2e463fb2025-04-17 11:22:22 -070021 InputSchema: json.RawMessage(doneChecklistJSONSchema(gitUsername, gitEmail)),
22 Run: func(ctx context.Context, input json.RawMessage) (string, error) {
23 // Cannot be done with a messy git.
24 if err := codereview.RequireNormalGitState(ctx); err != nil {
25 return "", err
26 }
27 if err := codereview.RequireNoUncommittedChanges(ctx); err != nil {
28 return "", err
29 }
30 // Ensure that the current commit has been reviewed.
31 head, err := codereview.CurrentCommit(ctx)
32 if err == nil {
33 needsReview := !codereview.IsInitialCommit(head) && !codereview.HasReviewed(head)
34 if needsReview {
35 return "", fmt.Errorf("codereview tool has not been run for commit %v", head)
36 }
37 }
38 return `Please ask the user to review your work. Be concise - users are more likely to read shorter comments.`, nil
39 },
40 }
41}
42
43func doneChecklistJSONSchema(gitUsername, gitEmail string) string {
Josh Bleecher Snyder75ec6bb2025-04-24 10:49:16 -070044 gitCommitDescription := fmt.Sprintf(`Create git commits for any code changes you made. Match the style of recent commit messages. Include 'Co-Authored-By: sketch <hello@sketch.dev>' and the original user prompt. Use GIT_AUTHOR_NAME="%s" GIT_AUTHOR_EMAIL="%s" (not git config).`,
Earl Lee2e463fb2025-04-17 11:22:22 -070045 gitUsername, gitEmail)
46 desc, err := json.Marshal(gitCommitDescription)
47 if err != nil {
48 panic(err)
49 }
50 return doneChecklistJSONSchemaPrefix + string(desc) + doneChecklistJSONSchemaSuffix
51}
52
53// TODO: this is ugly, maybe JSON-encode a deeply nested map[string]any instead? also ugly.
54const (
Josh Bleecher Snyderd7e56382025-05-05 13:22:08 -070055 doneDescription = `Use this tool when you have achieved the user's goal. The parameters form a checklist which you should evaluate.`
Earl Lee2e463fb2025-04-17 11:22:22 -070056 doneChecklistJSONSchemaPrefix = `{
57 "$schema": "http://json-schema.org/draft-07/schema#",
58 "title": "Checklist",
59 "description": "A schema for tracking checklist items with status and comments",
60 "type": "object",
61 "required": ["checklist_items"],
62 "properties": {
63 "checklist_items": {
64 "type": "object",
65 "description": "Collection of checklist items",
66 "properties": {
67 "wrote_tests": {
68 "$ref": "#/definitions/checklistItem",
69 "description": "If code was changed, tests were written or updated."
70 },
71 "passes_tests": {
72 "$ref": "#/definitions/checklistItem",
73 "description": "If any commits were made, tests pass."
74 },
75 "code_reviewed": {
76 "$ref": "#/definitions/checklistItem",
77 "description": "If any commits were made, the codereview tool was run and its output was addressed."
78 },
79 "git_commit": {
80 "$ref": "#/definitions/checklistItem",
81 "description": `
82
83 doneChecklistJSONSchemaSuffix = `
Josh Bleecher Snyderc6a2c242025-04-22 18:04:16 -070084 }
Earl Lee2e463fb2025-04-17 11:22:22 -070085 },
86 "additionalProperties": {
87 "$ref": "#/definitions/checklistItem"
88 }
89 }
90 },
91 "definitions": {
92 "checklistItem": {
93 "type": "object",
94 "required": ["status"],
95 "properties": {
96 "status": {
97 "type": "string",
98 "description": "Current status of the checklist item",
99 "enum": ["yes", "no", "not applicable", "other"]
100 },
101 "description": {
102 "type": "string",
103 "description": "Description of what this checklist item verifies"
104 },
105 "comments": {
106 "type": "string",
107 "description": "Additional comments or context for this checklist item"
108 }
109 }
110 }
111 }
112}`
113)