Skip to content

Commit 1ffcec3

Browse files
committed
feat: add focused system prompt to CLI provider
1 parent b2631cc commit 1ffcec3

4 files changed

Lines changed: 106 additions & 6 deletions

File tree

src/__tests__/prompts.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,50 @@
11
import {
22
buildInstruction,
33
buildContext,
4+
buildSystemPrompt,
45
} from '../prompts';
56

7+
describe('buildSystemPrompt', () => {
8+
it('returns a non-empty string', () => {
9+
const result = buildSystemPrompt();
10+
expect(typeof result).toBe('string');
11+
expect(result.length).toBeGreaterThan(0);
12+
});
13+
14+
it('establishes the commit-author role', () => {
15+
const result = buildSystemPrompt();
16+
expect(result.toLowerCase()).toContain('commit message');
17+
});
18+
19+
it('forbids markdown/code fences and preamble', () => {
20+
const result = buildSystemPrompt();
21+
expect(result).toContain('ONLY the commit message');
22+
expect(result.toLowerCase()).toContain('no markdown');
23+
expect(result.toLowerCase()).toContain('no code fences');
24+
});
25+
26+
it('enforces the 72-character subject rule', () => {
27+
const result = buildSystemPrompt();
28+
expect(result).toContain('72');
29+
});
30+
31+
it('mentions Conventional Commits as a conditional style', () => {
32+
const result = buildSystemPrompt();
33+
expect(result).toContain('Conventional Commits');
34+
});
35+
36+
it('instructs the model not to ask clarifying questions', () => {
37+
const result = buildSystemPrompt();
38+
expect(result.toLowerCase()).toContain('never ask');
39+
});
40+
41+
it('stays lean (short enough to be CLI-arg friendly)', () => {
42+
const result = buildSystemPrompt();
43+
// Sanity check: meaningfully shorter than Claude Code's default agentic prompt.
44+
expect(result.length).toBeLessThan(2000);
45+
});
46+
});
47+
648
describe('buildInstruction', () => {
749
it('returns a non-empty string', () => {
850
const result = buildInstruction(true);

src/__tests__/providers/cliProvider.test.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ describe('CliProvider', () => {
7878

7979
expect(mockSpawn).toHaveBeenCalledWith(
8080
'claude',
81-
['-p', 'my instruction', '--model', 'sonnet'],
81+
[
82+
'-p', 'my instruction',
83+
'--model', 'sonnet',
84+
'--system-prompt', expect.any(String),
85+
],
8286
{
8387
cwd: '/my/cwd',
8488
stdio: ['pipe', 'pipe', 'pipe'],
@@ -111,10 +115,30 @@ describe('CliProvider', () => {
111115

112116
expect(mockSpawn).toHaveBeenCalledWith(
113117
'claude',
114-
['-p', 'inst', '--model', 'opus'],
118+
[
119+
'-p', 'inst',
120+
'--model', 'opus',
121+
'--system-prompt', expect.any(String),
122+
],
115123
expect.any(Object)
116124
);
117125
});
126+
127+
it('passes a non-empty --system-prompt flag', async () => {
128+
const provider = new CliProvider('/cwd');
129+
const token = createMockCancellationToken();
130+
131+
const promise = provider.generateMessage('inst', 'ctx', token);
132+
mockProcess.emitClose(0);
133+
await promise;
134+
135+
const args = mockSpawn.mock.calls[0][1] as string[];
136+
const flagIndex = args.indexOf('--system-prompt');
137+
expect(flagIndex).toBeGreaterThanOrEqual(0);
138+
const value = args[flagIndex + 1];
139+
expect(typeof value).toBe('string');
140+
expect(value.length).toBeGreaterThan(0);
141+
});
118142
});
119143

120144
describe('errors', () => {

src/prompts.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
/**
2+
* Lean system prompt for the Claude CLI (`--system-prompt`).
3+
*
4+
* Replaces the default Claude Code agentic system prompt with a focused,
5+
* minimal persona so the model spends tokens on the commit message, not on
6+
* general-purpose tool preambles or clarifying chatter.
7+
*/
8+
export function buildSystemPrompt(): string {
9+
return [
10+
'You are ClawdCommit, a Git commit message author.',
11+
'Your only task is to produce one commit message for the staged diff the user provides via stdin.',
12+
'',
13+
'Output contract:',
14+
'- Emit ONLY the commit message text. No preamble, no commentary, no markdown, no code fences, no quotes.',
15+
'- Subject line: imperative mood, <=72 characters, no trailing period.',
16+
'- Body (only if the change warrants it): blank line after the subject, wrap around 72 columns, explain the why rather than restating the diff.',
17+
'- Match the project\'s existing commit style. Use Conventional Commits (type(scope): subject) when recent history shows that pattern.',
18+
'',
19+
'Behavior:',
20+
'- Never ask clarifying questions. Never request permission. Infer the best message from the diff and stop.',
21+
'- Do not invent changes that are not in the diff. Do not speculate about unrelated files.',
22+
'- If file-reading tools are available and a symbol\'s role is unclear from the diff alone, you may read the minimum needed; otherwise skip exploration.',
23+
].join('\n');
24+
}
25+
126
/** Build the instruction for commit message generation. */
227
export function buildInstruction(includeFileContext: boolean, canReadFiles: boolean = true): string {
328
const parts = [

src/providers/cliProvider.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { spawn, type ChildProcess } from 'child_process';
22
import * as vscode from 'vscode';
3+
import { buildSystemPrompt } from '../prompts';
34
import type { CommitMessageProvider, ClaudeModel } from './types';
45

56
const TIMEOUT_MS = 120_000;
@@ -21,10 +22,18 @@ export class CliProvider implements CommitMessageProvider {
2122
return;
2223
}
2324

24-
const child: ChildProcess = spawn('claude', ['-p', instruction, '--model', model], {
25-
cwd: this.cwd,
26-
stdio: ['pipe', 'pipe', 'pipe'],
27-
});
25+
const child: ChildProcess = spawn(
26+
'claude',
27+
[
28+
'-p', instruction,
29+
'--model', model,
30+
'--system-prompt', buildSystemPrompt(),
31+
],
32+
{
33+
cwd: this.cwd,
34+
stdio: ['pipe', 'pipe', 'pipe'],
35+
}
36+
);
2837

2938
const stdoutChunks: Buffer[] = [];
3039
const stderrChunks: Buffer[] = [];

0 commit comments

Comments
 (0)