From 139258b1e43c586ed654f205d26120f51cfecb49 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 21 Feb 2026 21:47:06 -0800 Subject: [PATCH 1/4] feat(cli): Add --staged flag and default to HEAD for uncommitted changes Config mode previously diffed against the default branch (e.g. main), which included all committed branch changes alongside uncommitted ones. Now both config mode and direct skill mode diff against HEAD by default, so `warden` with no args only analyzes uncommitted changes. Add --staged flag that uses `git diff --cached` to analyze only staged changes, supporting pre-commit workflows where you want to review exactly what you're about to commit. Co-Authored-By: Claude --- packages/docs/src/pages/guide.astro | 16 ++++++++++- src/cli/args.ts | 5 ++++ src/cli/commands/init.test.ts | 1 + src/cli/commands/logs.test.ts | 1 + src/cli/context.ts | 13 +++++++-- src/cli/git.ts | 44 +++++++++++++++++++++-------- src/cli/main.ts | 36 +++++++++++++++++------ 7 files changed, 91 insertions(+), 25 deletions(-) diff --git a/packages/docs/src/pages/guide.astro b/packages/docs/src/pages/guide.astro index 6e15eaf0..0283eadb 100644 --- a/packages/docs/src/pages/guide.astro +++ b/packages/docs/src/pages/guide.astro @@ -97,7 +97,21 @@ export WARDEN_ANTHROPIC_API_KEY=sk-ant-...`} /> -

Warden analyzes staged and unstaged changes, running any skills that match via your configured triggers.

+

Warden analyzes all uncommitted changes (staged and unstaged) against HEAD, running any skills that match via your configured triggers.

+ +

Review Only Staged Changes

+ +

For pre-commit workflows, analyze only what you're about to commit:

+ + + + + +

This uses git diff --cached so only staged files are analyzed.

Review Before Pushing

diff --git a/src/cli/args.ts b/src/cli/args.ts index 7c3ca53c..badc3ee1 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -37,6 +37,8 @@ export const CLIOptionsSchema = z.object({ list: z.boolean().default(false), /** Force interpretation of ambiguous targets as git refs */ git: z.boolean().default(false), + /** Analyze only staged changes (git diff --cached) */ + staged: z.boolean().default(false), /** Remote repository reference for skills (e.g., "owner/repo" or "owner/repo@sha") */ remote: z.string().optional(), /** Skip network operations - only use cached remote skills */ @@ -109,6 +111,7 @@ Options: --fix Automatically apply all suggested fixes --parallel Max concurrent trigger/skill executions (default: 4) -x, --fail-fast Stop after first finding + --staged Analyze only staged changes --git Force ambiguous targets to be treated as git refs --quiet Errors and final summary only -v, --verbose Show real-time findings and hunk details @@ -291,6 +294,7 @@ export function parseCliArgs(argv: string[] = process.argv.slice(2)): ParsedArgs 'fail-fast': { type: 'boolean', short: 'x', default: false }, parallel: { type: 'string' }, git: { type: 'boolean', default: false }, + staged: { type: 'boolean', default: false }, log: { type: 'boolean', default: false }, help: { type: 'boolean', short: 'h', default: false }, version: { type: 'boolean', short: 'V', default: false }, @@ -465,6 +469,7 @@ export function parseCliArgs(argv: string[] = process.argv.slice(2)): ParsedArgs force: values.force, parallel: values.parallel ? parseInt(values.parallel, 10) : undefined, git: values.git, + staged: values.staged, log: values.log, offline: values.offline, failFast: values['fail-fast'], diff --git a/src/cli/commands/init.test.ts b/src/cli/commands/init.test.ts index 2c4cc66b..82b24d19 100644 --- a/src/cli/commands/init.test.ts +++ b/src/cli/commands/init.test.ts @@ -26,6 +26,7 @@ function createOptions(overrides: Partial = {}): CLIOptions { force: false, list: false, git: false, + staged: false, offline: false, failFast: false, ...overrides, diff --git a/src/cli/commands/logs.test.ts b/src/cli/commands/logs.test.ts index fdcb4be4..7e77d8a9 100644 --- a/src/cli/commands/logs.test.ts +++ b/src/cli/commands/logs.test.ts @@ -27,6 +27,7 @@ function createDefaultOptions(overrides: Partial = {}): CLIOptions { force: false, list: false, git: false, + staged: false, offline: false, failFast: false, log: false, diff --git a/src/cli/context.ts b/src/cli/context.ts index fe8c4e4a..5f4b732f 100644 --- a/src/cli/context.ts +++ b/src/cli/context.ts @@ -34,6 +34,8 @@ export interface LocalContextOptions { cwd?: string; /** Override auto-detected default branch (from config) */ defaultBranch?: string; + /** Analyze only staged changes (git diff --cached) */ + staged?: boolean; } /** @@ -49,12 +51,14 @@ export function buildLocalEventContext(options: LocalContextOptions = {}): Event const { owner, name } = getRepoName(cwd); const defaultBranch = options.defaultBranch ?? getDefaultBranch(cwd); - const base = options.base ?? defaultBranch; + // When staged, always diff against HEAD (index vs HEAD) + const staged = options.staged ?? false; + const base = staged ? 'HEAD' : (options.base ?? defaultBranch); const head = options.head; // undefined means working tree const currentBranch = getCurrentBranch(cwd); const headSha = head ? head : getHeadSha(cwd); - const changedFiles = getChangedFilesWithPatches(base, head, cwd); + const changedFiles = getChangedFilesWithPatches(base, head, cwd, { staged }); const files = changedFiles.map(toFileChange); // Use actual commit message when analyzing a specific commit @@ -64,9 +68,12 @@ export function buildLocalEventContext(options: LocalContextOptions = {}): Event const commitMsg = getCommitMessage(head, cwd); title = commitMsg.subject || `Commit ${head}`; body = commitMsg.body || `Analyzing changes in ${head}`; + } else if (staged) { + title = `Staged changes: ${currentBranch}`; + body = `Analyzing staged changes`; } else { title = `Local changes: ${currentBranch}`; - body = `Analyzing local changes from ${base} to working tree`; + body = `Analyzing uncommitted changes from HEAD`; } return { diff --git a/src/cli/git.ts b/src/cli/git.ts index 4cbcf02a..3ac59ae6 100644 --- a/src/cli/git.ts +++ b/src/cli/git.ts @@ -170,18 +170,36 @@ function mapStatus(status: string): GitFileChange['status'] { } } +export interface DiffOptions { + /** Use --cached to diff only staged changes against HEAD */ + staged?: boolean; +} + +/** + * Build the git diff arguments for a given base/head/staged configuration. + */ +function buildDiffArgs(base: string, head: string | undefined, options?: DiffOptions): string[] { + if (options?.staged) { + return ['diff', '--cached']; + } + const diffRef = head ? `${base}...${head}` : base; + return ['diff', diffRef]; +} + /** * Get list of changed files between two refs. * If head is undefined, compares against the working tree. + * If options.staged is true, compares only staged changes against HEAD. */ export function getChangedFiles( base: string, head?: string, - cwd: string = process.cwd() + cwd: string = process.cwd(), + options?: DiffOptions ): GitFileChange[] { // Get file statuses - const diffRef = head ? `${base}...${head}` : base; - const nameStatusOutput = git(['diff', '--name-status', diffRef], cwd); + const baseArgs = buildDiffArgs(base, head, options); + const nameStatusOutput = git([...baseArgs, '--name-status'], cwd); if (!nameStatusOutput) { return []; @@ -207,7 +225,7 @@ export function getChangedFiles( } // Get numstat for additions/deletions - const numstatOutput = git(['diff', '--numstat', diffRef], cwd); + const numstatOutput = git([...baseArgs, '--numstat'], cwd); if (numstatOutput) { for (const line of numstatOutput.split('\n')) { if (!line.trim()) continue; @@ -233,11 +251,12 @@ export function getFilePatch( base: string, head: string | undefined, filename: string, - cwd: string = process.cwd() + cwd: string = process.cwd(), + options?: DiffOptions ): string | undefined { try { - const diffRef = head ? `${base}...${head}` : base; - return git(['diff', diffRef, '--', filename], cwd); + const baseArgs = buildDiffArgs(base, head, options); + return git([...baseArgs, '--', filename], cwd); } catch { return undefined; } @@ -276,9 +295,10 @@ function parseCombinedDiff(diffOutput: string): Map { export function getChangedFilesWithPatches( base: string, head?: string, - cwd: string = process.cwd() + cwd: string = process.cwd(), + options?: DiffOptions ): GitFileChange[] { - const files = getChangedFiles(base, head, cwd); + const files = getChangedFiles(base, head, cwd, options); if (files.length === 0) { return files; @@ -286,8 +306,8 @@ export function getChangedFilesWithPatches( // Get all patches in a single git diff command try { - const diffRef = head ? `${base}...${head}` : base; - const combinedDiff = git(['diff', diffRef], cwd); + const baseArgs = buildDiffArgs(base, head, options); + const combinedDiff = git(baseArgs, cwd); const patches = parseCombinedDiff(combinedDiff); for (const file of files) { @@ -297,7 +317,7 @@ export function getChangedFilesWithPatches( } catch { // Fall back to per-file patches if combined diff fails for (const file of files) { - file.patch = getFilePatch(base, head, file.filename, cwd); + file.patch = getFilePatch(base, head, file.filename, cwd, options); file.chunks = countPatchChunks(file.patch); } } diff --git a/src/cli/main.ts b/src/cli/main.ts index a7e321fc..ad034e0e 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -582,11 +582,14 @@ async function runConfigMode(options: CLIOptions, reporter: Reporter): Promise' - : undefined; - reporter.renderEmptyState('No changes found', tip); + if (options.staged) { + reporter.renderEmptyState('No staged changes found'); + } else { + const tip = !hasUncommittedChanges(repoPath) + ? 'Specify a git ref: warden HEAD~3 --skill ' + : undefined; + reporter.renderEmptyState('No uncommitted changes found', tip); + } reporter.blank(); } return 0; @@ -736,11 +743,13 @@ async function runDirectSkillMode(options: CLIOptions, reporter: Reporter): Prom const config = existsSync(configPath) ? loadWardenConfig(dirname(configPath)) : null; // Build context from local git - compare against HEAD for true uncommitted changes - reporter.startContext('Analyzing uncommitted changes...'); + const statusMessage = options.staged ? 'Analyzing staged changes...' : 'Analyzing uncommitted changes...'; + reporter.startContext(statusMessage); const context = buildLocalEventContext({ base: 'HEAD', cwd: repoPath, defaultBranch: config?.defaults?.defaultBranch, + staged: options.staged, }); const pullRequest = context.pullRequest; @@ -755,8 +764,12 @@ async function runDirectSkillMode(options: CLIOptions, reporter: Reporter): Prom process.stdout.write(content); } else { writeEmptyRunLog(repoPath, { traceId: getTraceId(), outputPath: options.output }); - const tip = 'Specify a git ref to analyze committed changes: warden main --skill '; - reporter.renderEmptyState('No uncommitted changes found', tip); + if (options.staged) { + reporter.renderEmptyState('No staged changes found'); + } else { + const tip = 'Specify a git ref to analyze committed changes: warden main --skill '; + reporter.renderEmptyState('No uncommitted changes found', tip); + } reporter.blank(); } return 0; @@ -770,6 +783,11 @@ async function runDirectSkillMode(options: CLIOptions, reporter: Reporter): Prom async function runCommand(options: CLIOptions, reporter: Reporter): Promise { const targets = options.targets ?? []; + // --staged is only meaningful without explicit targets + if (options.staged && targets.length > 0) { + reporter.warning('--staged is ignored when targets are specified'); + } + // No targets with --skill → run skill directly on uncommitted changes if (targets.length === 0 && options.skill) { return runDirectSkillMode(options, reporter); From 9f2d56bb968de9af1d9e3caeeb9e235886ecc7c7 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sun, 22 Feb 2026 10:49:50 -0800 Subject: [PATCH 2/4] fix: Server error handler calls process.exit() bypassing cleanup Warden finding find-warden-bugs-19c99a7a Severity: high Co-Authored-By: Warden --- src/cli/commands/setup-app.test.ts | 221 +++++++++++++++++++++++++++++ src/cli/commands/setup-app.ts | 32 +++-- 2 files changed, 239 insertions(+), 14 deletions(-) create mode 100644 src/cli/commands/setup-app.test.ts diff --git a/src/cli/commands/setup-app.test.ts b/src/cli/commands/setup-app.test.ts new file mode 100644 index 00000000..e8efc039 --- /dev/null +++ b/src/cli/commands/setup-app.test.ts @@ -0,0 +1,221 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { EventEmitter } from 'node:events'; +import { runSetupApp } from './setup-app.js'; +import { Reporter, parseVerbosity } from '../output/index.js'; +import type { SetupAppOptions } from '../args.js'; + +// Mock all external dependencies so we never hit the network or filesystem. +vi.mock('./setup-app/manifest.js', () => ({ + buildManifest: () => ({ name: 'test-app', url: 'http://localhost', public: false }), +})); + +vi.mock('./setup-app/browser.js', () => ({ + openBrowser: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock('./setup-app/credentials.js', () => ({ + exchangeCodeForCredentials: vi.fn().mockResolvedValue({ + id: 12345, + name: 'test-app', + pem: '-----BEGIN RSA PRIVATE KEY-----\nfake\n-----END RSA PRIVATE KEY-----', + htmlUrl: 'https://github.com/apps/test-app', + }), +})); + +vi.mock('../git.js', () => ({ + getGitHubRepoUrl: () => 'https://github.com/test/repo', +})); + +// We need fine-grained control over the callback server mock, so we build it +// per-test in a factory that the mock delegates to. +let serverFactory: () => ReturnType; + +vi.mock('./setup-app/server.js', () => ({ + startCallbackServer: (...args: unknown[]) => serverFactory(), +})); + +function createTestReporter(): Reporter { + const mode = { isTTY: false, supportsColor: false, columns: 80 }; + return new Reporter(mode, parseVerbosity(false, 0, false)); +} + +function createOptions(overrides: Partial = {}): SetupAppOptions { + return { + port: 3456, + timeout: 60, + open: false, + ...overrides, + }; +} + +/** + * Build a fake server handle whose `server` is an EventEmitter so we can + * simulate 'error' events. The `waitForCallback` promise can be resolved or + * rejected externally. + */ +function createMockServerHandle() { + const emitter = new EventEmitter(); + let resolveCallback!: (v: { code: string }) => void; + let rejectCallback!: (e: Error) => void; + const waitForCallback = new Promise<{ code: string }>((resolve, reject) => { + resolveCallback = resolve; + rejectCallback = reject; + }); + const close = vi.fn(); + + return { + handle: { + server: emitter, + waitForCallback, + close, + startUrl: 'http://localhost:3456/start', + }, + resolveCallback, + rejectCallback, + close, + }; +} + +describe('runSetupApp', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + describe('server error handling', () => { + it('returns exit code 1 and calls close() when the server emits EADDRINUSE', async () => { + const mock = createMockServerHandle(); + serverFactory = () => mock.handle; + + const reporter = createTestReporter(); + const errorSpy = vi.spyOn(reporter, 'error'); + + // Emit the error on the next microtask so `runSetupApp` has time to + // register its listener and start awaiting the promise race. + const errorObj: NodeJS.ErrnoException = new Error('listen EADDRINUSE: address already in use'); + errorObj.code = 'EADDRINUSE'; + + setTimeout(() => mock.handle.server.emit('error', errorObj), 5); + + const exitCode = await runSetupApp(createOptions(), reporter); + + expect(exitCode).toBe(1); + expect(mock.close).toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('already in use'), + ); + }); + + it('returns exit code 1 for generic server errors', async () => { + const mock = createMockServerHandle(); + serverFactory = () => mock.handle; + + const reporter = createTestReporter(); + const errorSpy = vi.spyOn(reporter, 'error'); + + const errorObj: NodeJS.ErrnoException = new Error('something broke'); + errorObj.code = 'EACCES'; + + setTimeout(() => mock.handle.server.emit('error', errorObj), 5); + + const exitCode = await runSetupApp(createOptions(), reporter); + + expect(exitCode).toBe(1); + expect(mock.close).toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('Server error: something broke'), + ); + }); + + it('does not call process.exit on server error', async () => { + const mock = createMockServerHandle(); + serverFactory = () => mock.handle; + + const reporter = createTestReporter(); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as never); + + const errorObj: NodeJS.ErrnoException = new Error('port in use'); + errorObj.code = 'EADDRINUSE'; + + setTimeout(() => mock.handle.server.emit('error', errorObj), 5); + + await runSetupApp(createOptions(), reporter); + + expect(exitSpy).not.toHaveBeenCalled(); + }); + }); + + describe('cleanup on errors', () => { + it('calls serverHandle.close() even when waitForCallback rejects', async () => { + const mock = createMockServerHandle(); + serverFactory = () => mock.handle; + + const reporter = createTestReporter(); + + setTimeout(() => mock.rejectCallback(new Error('Timeout')), 5); + + const exitCode = await runSetupApp(createOptions(), reporter); + + expect(exitCode).toBe(1); + expect(mock.close).toHaveBeenCalled(); + }); + }); + + describe('happy path', () => { + it('returns exit code 0 on successful callback exchange', async () => { + const mock = createMockServerHandle(); + serverFactory = () => mock.handle; + + const reporter = createTestReporter(); + + setTimeout(() => mock.resolveCallback({ code: 'test-code' }), 5); + + const exitCode = await runSetupApp(createOptions(), reporter); + + expect(exitCode).toBe(0); + expect(mock.close).toHaveBeenCalled(); + }); + }); + + describe('URL display', () => { + it('shows URL when open is false', async () => { + const mock = createMockServerHandle(); + serverFactory = () => mock.handle; + + const reporter = createTestReporter(); + const textSpy = vi.spyOn(reporter, 'text'); + + setTimeout(() => mock.resolveCallback({ code: 'test-code' }), 5); + + await runSetupApp(createOptions({ open: false }), reporter); + + const textCalls = textSpy.mock.calls.map((c) => c[0]); + expect(textCalls).toContainEqual( + expect.stringContaining('Open this URL in your browser'), + ); + }); + + it('shows URL when browser open fails', async () => { + const { openBrowser } = await import('./setup-app/browser.js'); + vi.mocked(openBrowser).mockRejectedValueOnce(new Error('xdg-open not found')); + + const mock = createMockServerHandle(); + serverFactory = () => mock.handle; + + const reporter = createTestReporter(); + const textSpy = vi.spyOn(reporter, 'text'); + const warnSpy = vi.spyOn(reporter, 'warning'); + + setTimeout(() => mock.resolveCallback({ code: 'test-code' }), 5); + + await runSetupApp(createOptions({ open: true }), reporter); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Could not open browser'), + ); + const textCalls = textSpy.mock.calls.map((c) => c[0]); + expect(textCalls).toContainEqual( + expect.stringContaining('Open this URL in your browser'), + ); + }); + }); +}); diff --git a/src/cli/commands/setup-app.ts b/src/cli/commands/setup-app.ts index 3d88d504..2cba6c4b 100644 --- a/src/cli/commands/setup-app.ts +++ b/src/cli/commands/setup-app.ts @@ -48,29 +48,33 @@ export async function runSetupApp(options: SetupAppOptions, reporter: Reporter): org, }); - // Handle server errors (e.g., port already in use) - serverHandle.server.on('error', (error: NodeJS.ErrnoException) => { - if (error.code === 'EADDRINUSE') { - reporter.error(`Port ${port} is already in use. Try a different port with --port `); - } else { - reporter.error(`Server error: ${error.message}`); - } - process.exit(1); + // Handle server errors (e.g., port already in use) by racing a rejection + // against the callback promise so the error flows into the catch block below. + const serverError = new Promise((_, reject) => { + serverHandle.server.on('error', (error: NodeJS.ErrnoException) => { + if (error.code === 'EADDRINUSE') { + reject(new Error(`Port ${port} is already in use. Try a different port with --port `)); + } else { + reject(new Error(`Server error: ${error.message}`)); + } + }); }); try { // Open browser to our local server (which will POST to GitHub) + let showUrl = !open; if (open) { reporter.step('Opening browser...'); try { await openBrowser(serverHandle.startUrl); } catch { reporter.warning('Could not open browser automatically.'); - reporter.blank(); - reporter.text('Open this URL in your browser:'); - reporter.text(chalk.cyan(serverHandle.startUrl)); + showUrl = true; } - } else { + } + + // Show the URL if the browser was not opened (or failed to open) + if (showUrl) { reporter.blank(); reporter.text('Open this URL in your browser:'); reporter.text(chalk.cyan(serverHandle.startUrl)); @@ -81,8 +85,8 @@ export async function runSetupApp(options: SetupAppOptions, reporter: Reporter): reporter.blank(); reporter.text(chalk.dim('Waiting for GitHub callback... (Ctrl+C to cancel)')); - // Wait for callback - const { code } = await serverHandle.waitForCallback; + // Wait for callback, but also abort if the server errors out + const { code } = await Promise.race([serverHandle.waitForCallback, serverError]); // Exchange code for credentials reporter.blank(); From 31ca9a0ec6f569cd9c751e4a0d2c368532aa713c Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 23 Feb 2026 10:02:47 -0800 Subject: [PATCH 3/4] fix(cli): Prevent unhandled rejection from server error during openBrowser Attach a no-op catch to the serverError promise immediately so that if the server errors before Promise.race is reached (e.g. during openBrowser), it doesn't cause an unhandled rejection. Also fix pre-existing lint errors in setup-app tests. Co-Authored-By: Claude Opus 4.6 --- src/cli/commands/setup-app.test.ts | 7 ++++--- src/cli/commands/setup-app.ts | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cli/commands/setup-app.test.ts b/src/cli/commands/setup-app.test.ts index e8efc039..1f9aecbe 100644 --- a/src/cli/commands/setup-app.test.ts +++ b/src/cli/commands/setup-app.test.ts @@ -28,10 +28,11 @@ vi.mock('../git.js', () => ({ // We need fine-grained control over the callback server mock, so we build it // per-test in a factory that the mock delegates to. -let serverFactory: () => ReturnType; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let serverFactory: () => any; vi.mock('./setup-app/server.js', () => ({ - startCallbackServer: (...args: unknown[]) => serverFactory(), + startCallbackServer: (..._args: unknown[]) => serverFactory(), })); function createTestReporter(): Reporter { @@ -131,7 +132,7 @@ describe('runSetupApp', () => { serverFactory = () => mock.handle; const reporter = createTestReporter(); - const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as never); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never); const errorObj: NodeJS.ErrnoException = new Error('port in use'); errorObj.code = 'EADDRINUSE'; diff --git a/src/cli/commands/setup-app.ts b/src/cli/commands/setup-app.ts index 2cba6c4b..a59cd1cb 100644 --- a/src/cli/commands/setup-app.ts +++ b/src/cli/commands/setup-app.ts @@ -59,6 +59,8 @@ export async function runSetupApp(options: SetupAppOptions, reporter: Reporter): } }); }); + // Prevent unhandled rejection if the server errors before Promise.race is reached + serverError.catch((_e: unknown) => undefined); try { // Open browser to our local server (which will POST to GitHub) From 8cc3a9b03e14da5b8996c6f8bb3d8cd8358d924e Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 23 Feb 2026 10:26:13 -0800 Subject: [PATCH 4/4] fix(cli): Use actual base ref in local changes body text Interpolate the base variable instead of hardcoding "HEAD" so the body accurately reflects the base ref (e.g. 'main') when running in git ref mode. Co-Authored-By: Claude Opus 4.6 --- src/cli/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/context.ts b/src/cli/context.ts index 5f4b732f..88b22b45 100644 --- a/src/cli/context.ts +++ b/src/cli/context.ts @@ -73,7 +73,7 @@ export function buildLocalEventContext(options: LocalContextOptions = {}): Event body = `Analyzing staged changes`; } else { title = `Local changes: ${currentBranch}`; - body = `Analyzing uncommitted changes from HEAD`; + body = `Analyzing local changes from ${base} to working tree`; } return {