Skip to content

Commit 7126ba9

Browse files
ericdalloeca-agent
andcommitted
Add dedicated git tool with condensed prompt, reducing per-turn token overhead
🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca <git@eca.dev>
1 parent 830a017 commit 7126ba9

File tree

6 files changed

+172
-111
lines changed

6 files changed

+172
-111
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Show the selected `variant` in `spawn_agent` subagent details and harden restrictions regarding the use of the optional model in sub-agent. #369
66
- Fix MCP OAuth browser not opening on Windows by using `cmd /c start` instead of `java.awt.Desktop`, which is unavailable in the native image.
77
- Improve server shutdown speed for remote MCP servers.
8+
- Extract git/gh instructions from `shell_command` into a dedicated `git` tool with a condensed prompt, reducing per-turn token overhead.
89

910
## 0.117.1
1011

resources/prompts/tools/git.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Runs git and gh (GitHub CLI) commands.
2+
3+
Always provide the `operation` that best describes what the command does.
4+
Provide a concise `summary` (2-3 words) of what is being done.
5+
6+
# Commits
7+
8+
1. First run: `git status`, `git diff` (staged + unstaged), `git log` (for commit style).
9+
2. Analyze: files changed, nature/purpose, sensitive info check, draft a "why"-focused message.
10+
3. Stage only relevant files (avoid blind `git add .`), then commit via HEREDOC:
11+
12+
<example>
13+
git commit -m "$(cat <<'EOF'
14+
Commit message here.
15+
16+
🤖 Generated with [eca](https://eca.dev)
17+
18+
Co-Authored-By: eca <git@eca.dev>
19+
EOF
20+
)"
21+
</example>
22+
23+
4. If pre-commit hooks modify files, amend or retry ONCE. If it fails again, stop.
24+
25+
# Pull Requests
26+
27+
1. First run: `git status`, `git diff`, `git log`, `git diff main...HEAD` to review ALL commits since diverging.
28+
2. Analyze: all commits (not just latest), nature/purpose/impact, draft 1-3 bullet summary.
29+
3. Push with `-u` if needed, then create PR via HEREDOC:
30+
31+
<example>
32+
gh pr create --title "the pr title" --body "$(cat <<'EOF'
33+
## Summary
34+
<1-3 bullet points>
35+
36+
## Test plan
37+
[Testing checklist]
38+
39+
🤖 Generated with [eca](https://eca.dev)
40+
EOF
41+
)"
42+
</example>
43+
44+
4. Return the PR URL.
45+
46+
# Rules
47+
- Never use interactive flags (`-i`, `--interactive`)
48+
- Never update git config
49+
- Do not push unless creating a PR
50+
- Do not create empty commits
51+
- Always use HEREDOC for multi-line messages
52+
- Use `gh` for all GitHub API interactions (issues, PRs, checks, releases)
53+
- Given a GitHub URL, use `gh` to fetch the information

resources/prompts/tools/shell_command.md

Lines changed: 0 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -28,110 +28,3 @@ Usage notes:
2828
<bad-example>
2929
cd /foo/bar && pytest tests
3030
</bad-example>
31-
32-
# Committing changes with git
33-
34-
When the user asks you to create a new git commit, follow these steps carefully:
35-
36-
1.:
37-
- Run a git status command to see all untracked files.
38-
- Run a git diff command to see both staged and unstaged changes that will be committed.
39-
- Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
40-
41-
2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in <commit_analysis> tags:
42-
43-
<commit_analysis>
44-
- List the files that have been changed or added
45-
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
46-
- Brainstorm the purpose or motivation behind these changes
47-
- Assess the impact of these changes on the overall project
48-
- Check for any sensitive information that shouldn't be committed
49-
- Draft a concise (1-2 sentences) commit message that focuses on the \"why\" rather than the \"what\"
50-
- Ensure your language is clear, concise, and to the point
51-
- Ensure the message accurately reflects the changes and their purpose (i.e. \"add\" means a wholly new feature, \" update \" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.)
52-
- Ensure the message is not generic (avoid words like \"Update\" or \"Fix\" without context)
53-
- Review the draft message to ensure it accurately reflects the changes and their purpose
54-
</commit_analysis>
55-
56-
3.:
57-
- Add relevant untracked files to the staging area.
58-
- Create the commit with a message ending with:
59-
🤖 Generated with [eca](https://eca.dev)
60-
61-
Co-Authored-By: eca <noreply@eca.dev>
62-
- Run git status to make sure the commit succeeded.
63-
64-
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
65-
66-
Important notes:
67-
- Use the git context at the start of this conversation to determine which files are relevant to your commit. Be careful not to stage and commit files (e.g. with `git add .`) that aren't relevant to your commit.
68-
- NEVER update the git config
69-
- DO NOT run additional commands to read or explore code, beyond what is available in the git context
70-
- DO NOT push to the remote repository
71-
- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
72-
- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
73-
- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.
74-
- Return an empty response - the user will see the git output directly
75-
- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
76-
<example>
77-
git commit -m \"$(cat <<'EOF'
78-
Commit message here.
79-
80-
🤖 Generated with [eca](https://eca.dev)
81-
82-
Co-Authored-By: eca <noreply@eca.dev>
83-
EOF
84-
)\"
85-
</example>
86-
87-
# Creating pull requests
88-
Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
89-
90-
IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
91-
92-
1. In order to understand the current state of the branch since it diverged from the main branch:
93-
- Run a git status command to see all untracked files
94-
- Run a git diff command to see both staged and unstaged changes that will be committed
95-
- Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
96-
- Run a git log command and `git diff main...HEAD` to understand the full commit history for the current branch (from the time it diverged from the `main` branch)
97-
98-
2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary. Wrap your analysis process in <pr_analysis> tags:
99-
100-
<pr_analysis>
101-
- List the commits since diverging from the main branch
102-
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
103-
- Brainstorm the purpose or motivation behind these changes
104-
- Assess the impact of these changes on the overall project
105-
- Do not use tools to explore code, beyond what is available in the git context
106-
- Check for any sensitive information that shouldn't be committed
107-
- Draft a concise (1-2 bullet points) pull request summary that focuses on the \"why\" rather than the \"what\"
108-
- Ensure the summary accurately reflects all changes since diverging from the main branch
109-
- Ensure your language is clear, concise, and to the point
110-
- Ensure the summary accurately reflects the changes and their purpose (ie. \"add\" means a wholly new feature, " update " means an enhancement to an existing feature, \"fix\" means a bug fix, etc.)
111-
- Ensure the summary is not generic (avoid words like \"Update\" or \"Fix\" without context)
112-
- Review the draft summary to ensure it accurately reflects the changes and their purpose
113-
</pr_analysis>
114-
115-
3. ALWAYS run the following commands in parallel:
116-
- Create new branch if needed
117-
- Push to remote with -u flag if needed
118-
- Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
119-
<example>
120-
gh pr create --title \"the pr title\" --body \"$(cat <<'EOF'
121-
## Summary
122-
<1-3 bullet points>
123-
124-
## Test plan
125-
[Checklist of TODOs for testing the pull request...]
126-
127-
🤖 Generated with [eca](https://eca.dev)
128-
EOF
129-
)\"
130-
</example>
131-
132-
Important:
133-
- NEVER update the git config
134-
- Return the PR URL when you're done, so the user can see it
135-
136-
# Other common operations
137-
- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments

src/eca/config.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
:disabledTools ["preview_file_change"]}
9898
"plan" {:mode "primary"
9999
:prompts {:chat "${classpath:prompts/plan_agent.md}"}
100-
:disabledTools ["edit_file" "write_file" "move_file"]
100+
:disabledTools ["edit_file" "write_file" "move_file" "git"]
101101
:toolCall {:approval {:byDefault "ask"
102102
:allow {"eca__shell_command"
103103
{:argsMatchers {"command" ["pwd"
@@ -120,7 +120,7 @@
120120
"explorer" {:mode "subagent"
121121
:description "${classpath:prompts/explorer_agent_description.md}"
122122
:systemPrompt "${classpath:prompts/explorer_agent.md}"
123-
:disabledTools ["edit_file" "write_file" "move_file" "preview_file_change"]
123+
:disabledTools ["edit_file" "write_file" "move_file" "preview_file_change" "git"]
124124
:toolCall {:approval {:byDefault "ask"
125125
:allow {"eca__shell_command"
126126
{:argsMatchers {"command" ["pwd"

src/eca/features/tools.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[eca.features.tools.custom :as f.tools.custom]
1010
[eca.features.tools.editor :as f.tools.editor]
1111
[eca.features.tools.filesystem :as f.tools.filesystem]
12+
[eca.features.tools.git :as f.tools.git]
1213
[eca.features.tools.mcp :as f.mcp]
1314
[eca.features.tools.mcp.clojure-mcp]
1415
[eca.features.tools.shell :as f.tools.shell]
@@ -153,6 +154,7 @@
153154
(merge {}
154155
f.tools.filesystem/definitions
155156
f.tools.shell/definitions
157+
f.tools.git/definitions
156158
f.tools.editor/definitions
157159
f.tools.chat/definitions
158160
f.tools.skill/definitions
@@ -167,9 +169,10 @@
167169
"Filter tools for subagent execution.
168170
169171
- Excludes spawn_agent to prevent nesting.
170-
- Excludes task because task list state is currently chat-local; it should be managed by the parent agent."
172+
- Excludes task because task list state is currently chat-local; it should be managed by the parent agent.
173+
- Excludes git because subagents don't perform git operations."
171174
[tools]
172-
(filterv #(not (contains? #{"spawn_agent" "task"} (:name %))) tools))
175+
(filterv #(not (contains? #{"spawn_agent" "task" "git"} (:name %))) tools))
173176

174177
(defn all-tools
175178
"Returns all available tools, including both native ECA tools

src/eca/features/tools/git.clj

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
(ns eca.features.tools.git
2+
(:require
3+
[babashka.process :as p]
4+
[clojure.string :as string]
5+
[eca.features.tools.shell :as shell]
6+
[eca.features.tools.util :as tools.util]
7+
[eca.logger :as logger]
8+
[eca.shared :as shared]))
9+
10+
(set! *warn-on-reflection* true)
11+
12+
(def ^:private logger-tag "[TOOLS-GIT]")
13+
14+
(def ^:private default-timeout 120000)
15+
16+
(defn ^:private git-command [arguments {:keys [db tool-call-id call-state-fn state-transition-fn]}]
17+
(let [command (get arguments "command")]
18+
(or (tools.util/invalid-arguments
19+
arguments
20+
[["command" #(and (string? %)
21+
(let [trimmed (string/triml %)
22+
cmd (if (string/starts-with? trimmed "cd ")
23+
(or (some->> (re-find #"^cd\s+\S+\s*(?:&&|;)\s*(.*)" trimmed)
24+
second
25+
string/triml)
26+
trimmed)
27+
trimmed)]
28+
(or (string/starts-with? cmd "git ")
29+
(string/starts-with? cmd "git;")
30+
(string/starts-with? cmd "gh "))))
31+
"command must start with 'git' or 'gh' (optionally preceded by 'cd <path> &&')"]])
32+
(let [work-dir (or (some-> (:workspace-folders db)
33+
first
34+
:uri
35+
shared/uri->filename)
36+
(System/getProperty "user.home"))
37+
_ (logger/debug logger-tag "Running command:" command)
38+
result (try
39+
(if-let [proc (when-not (= :stopping (:status (call-state-fn)))
40+
(shell/start-shell-process! {:cwd work-dir
41+
:script command}))]
42+
(do
43+
(state-transition-fn :resources-created {:resources {:process proc}})
44+
(try (deref proc default-timeout ::timeout)
45+
(catch InterruptedException e
46+
(let [msg (or (.getMessage e) "Git tool call was interrupted")]
47+
(logger/debug logger-tag "Git tool call was interrupted"
48+
{:tool-call-id tool-call-id :message msg})
49+
(tools.util/tool-call-destroy-resource! "eca__git" :process proc)
50+
(state-transition-fn :resources-destroyed {:resources [:process]})
51+
{:exit 1 :err msg}))))
52+
{:exit 1 :err "Tool call is :stopping, so process not spawned"})
53+
(catch Exception e
54+
(let [msg (or (.getMessage e) "Caught an Exception during execution of the git tool")]
55+
(logger/warn logger-tag "Got an Exception during execution" {:message msg})
56+
{:exit 1 :err msg}))
57+
(finally
58+
(let [state (call-state-fn)]
59+
(when-let [resources (:resources state)]
60+
(doseq [[res-kwd res] resources]
61+
(tools.util/tool-call-destroy-resource! "eca__git" res-kwd res))
62+
(when (#{:executing :stopping} (:status state))
63+
(state-transition-fn :resources-destroyed {:resources (keys resources)}))))))]
64+
(if (= result ::timeout)
65+
(do
66+
(logger/debug logger-tag "Command timed out after" default-timeout "ms")
67+
(tools.util/single-text-content (str "Command timed out after " default-timeout " ms") true))
68+
(do
69+
(logger/debug logger-tag "Command executed:" result)
70+
{:error (not (zero? (:exit result)))
71+
:contents (remove nil?
72+
(concat [{:type :text
73+
:text (str "Exit code: " (:exit result))}]
74+
(when-not (string/blank? (:err result))
75+
[{:type :text
76+
:text (str "Stderr:\n" (string/trim (:err result)))}])
77+
(when-not (string/blank? (:out result))
78+
[{:type :text
79+
:text (str "Stdout:\n" (string/trim (:out result)))}])))}))))))
80+
81+
(defn ^:private git-command-summary [{:keys [args]}]
82+
(let [operation (get args "operation")
83+
summary (get args "summary")]
84+
(if operation
85+
(cond-> (str "Git " operation)
86+
summary (str ": " summary))
87+
"Preparing git operation")))
88+
89+
(def definitions
90+
{"git"
91+
{:description (tools.util/read-tool-description "git")
92+
:parameters {:type "object"
93+
:properties {"command" {:type "string"
94+
:description "The git or gh command to execute."}
95+
"operation" {:type "string"
96+
:description "The git operation being performed."
97+
:enum ["status" "diff" "log" "add" "commit" "push"
98+
"branch" "checkout" "merge" "rebase" "stash"
99+
"pr_create" "pr_view" "pr_comment" "gh_other"]}
100+
"summary" {:type "string"
101+
:description "Concise 2-3 word description of what is being done (e.g. \"staged changes\", \"all test files\")."}}
102+
:required ["command" "operation"]}
103+
:handler #'git-command
104+
:summary-fn #'git-command-summary}})
105+
106+
(defmethod tools.util/tool-call-destroy-resource! :eca__git [name resource-kwd resource]
107+
(logger/debug logger-tag "About to destroy resource" {:resource-kwd resource-kwd})
108+
(case resource-kwd
109+
:process (p/destroy-tree resource)
110+
(logger/warn logger-tag "Unknown resource keyword" {:tool-name name
111+
:resource-kwd resource-kwd})))

0 commit comments

Comments
 (0)