Add git_commit_message, written from the staged diff
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
co-authored by
Sisyphus
parent
6b67465723
commit
e8b8b06a4b
@@ -0,0 +1,74 @@
|
||||
import { tool, generateText, type LanguageModel } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { git } from './tools-git';
|
||||
|
||||
/** Recent subjects shown to the model, so the message matches the repository's style. */
|
||||
const SUBJECTS = 15;
|
||||
/** The staged diff is the bulk of the call; beyond this it is cut with a note. */
|
||||
const MAX_DIFF = 24_000;
|
||||
|
||||
export const COMMIT_TOOL_NAME = 'git_commit_message';
|
||||
|
||||
/** A reply's wrapping — fenced blocks, surrounding prose, leading/trailing quotes. */
|
||||
const unwrap = (reply: string): string => {
|
||||
const fenced = /```[a-z]*\n([\s\S]*?)```/i.exec(reply);
|
||||
const body = fenced ? fenced[1]! : reply;
|
||||
const line = body.split('\n').find((l) => l.trim().length > 0) ?? '';
|
||||
return line.trim().replace(/^["'`]|["'`]$/g, '').slice(0, 72);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a commit message from the staged changes with one nested model call.
|
||||
*
|
||||
* The model sees two things: the staged diff, and the repository's own recent
|
||||
* subjects, because a message that ignores the established style reads as foreign
|
||||
* no matter how accurate it is. The subject style of this repository — plain
|
||||
* imperative, no conventional-commit prefix — is one example; the sample keeps
|
||||
* the choice local to whatever the history actually says.
|
||||
*
|
||||
* It never commits. Generating the message is safe to auto-approve; running the
|
||||
* commit is not, and that stays on the gated `bash` path where the user sees the
|
||||
* message and the command together.
|
||||
*/
|
||||
export function createCommitMessageTool(opts: { model: LanguageModel; cwd?: string }) {
|
||||
return tool({
|
||||
description:
|
||||
'Generate a commit message from the staged changes, in one nested model call. Reads the ' +
|
||||
'staged diff and the recent commit subjects so the message matches the repository\'s style. ' +
|
||||
'It does not commit — it returns the message only. Use git_diff first to see what is staged, ' +
|
||||
'and run the commit through bash where the user approves it.',
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
const cwd = opts.cwd ?? process.cwd();
|
||||
|
||||
const staged = await git(['diff', '--staged', '--no-color'], cwd);
|
||||
if (!staged.ok) throw new Error(staged.message);
|
||||
const diff = staged.stdout.trim();
|
||||
if (diff.length === 0) return 'Nothing is staged. Stage the change first, then ask again.';
|
||||
|
||||
const subjects = await git(
|
||||
['log', `-n${SUBJECTS}`, '--pretty=format:%s'],
|
||||
cwd,
|
||||
);
|
||||
const history = subjects.ok && subjects.stdout.trim().length > 0 ? subjects.stdout : '(no commits yet)';
|
||||
|
||||
const shown =
|
||||
diff.length > MAX_DIFF ? `${diff.slice(0, MAX_DIFF)}\n... [truncated ${diff.length - MAX_DIFF} chars]` : diff;
|
||||
|
||||
const { text } = await generateText({
|
||||
model: opts.model,
|
||||
system:
|
||||
'You write one commit message for the staged diff below. Match the subject style of the ' +
|
||||
'recent commits listed after it: same language, same capitalisation, same prefix convention ' +
|
||||
'or lack of one. One line, no body, no quotes, no backticks, no prefix like "commit:". ' +
|
||||
'Describe what the change does, not what files it touches.',
|
||||
prompt: `Staged diff:\n\n${shown}\n\nRecent commit subjects:\n${history}`,
|
||||
maxRetries: 2,
|
||||
});
|
||||
|
||||
const message = unwrap(text);
|
||||
if (message.length === 0) throw new Error('the model returned no message; ask again or write one yourself');
|
||||
return message;
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user