タスク名を AI で英語スラグに要約する機能を追加
worktree 作成時、日本語タスク名を英語ブランチ名に変換。 - summarizeTaskName() を追加(Claude で要約) - 例: 「認証機能を追加する」→ add-auth
This commit is contained in:
parent
9eb63e787e
commit
82193e6db4
@ -18,6 +18,10 @@ vi.mock('../task/autoCommit.js', () => ({
|
|||||||
autoCommitWorktree: vi.fn(),
|
autoCommitWorktree: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../task/summarize.js', () => ({
|
||||||
|
summarizeTaskName: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../utils/ui.js', () => ({
|
vi.mock('../utils/ui.js', () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
@ -73,11 +77,13 @@ vi.mock('../constants.js', () => ({
|
|||||||
|
|
||||||
import { confirm } from '../prompt/index.js';
|
import { confirm } from '../prompt/index.js';
|
||||||
import { createWorktree } from '../task/worktree.js';
|
import { createWorktree } from '../task/worktree.js';
|
||||||
|
import { summarizeTaskName } from '../task/summarize.js';
|
||||||
import { info } from '../utils/ui.js';
|
import { info } from '../utils/ui.js';
|
||||||
import { confirmAndCreateWorktree } from '../cli.js';
|
import { confirmAndCreateWorktree } from '../cli.js';
|
||||||
|
|
||||||
const mockConfirm = vi.mocked(confirm);
|
const mockConfirm = vi.mocked(confirm);
|
||||||
const mockCreateWorktree = vi.mocked(createWorktree);
|
const mockCreateWorktree = vi.mocked(createWorktree);
|
||||||
|
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
|
||||||
const mockInfo = vi.mocked(info);
|
const mockInfo = vi.mocked(info);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -96,11 +102,13 @@ describe('confirmAndCreateWorktree', () => {
|
|||||||
expect(result.execCwd).toBe('/project');
|
expect(result.execCwd).toBe('/project');
|
||||||
expect(result.isWorktree).toBe(false);
|
expect(result.isWorktree).toBe(false);
|
||||||
expect(mockCreateWorktree).not.toHaveBeenCalled();
|
expect(mockCreateWorktree).not.toHaveBeenCalled();
|
||||||
|
expect(mockSummarizeTaskName).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create worktree and return worktree path when user confirms', async () => {
|
it('should create worktree and return worktree path when user confirms', async () => {
|
||||||
// Given: user says "yes" to worktree creation
|
// Given: user says "yes" to worktree creation
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
mockSummarizeTaskName.mockResolvedValue('fix-auth');
|
||||||
mockCreateWorktree.mockReturnValue({
|
mockCreateWorktree.mockReturnValue({
|
||||||
path: '/project/.takt/worktrees/20260128T0504-fix-auth',
|
path: '/project/.takt/worktrees/20260128T0504-fix-auth',
|
||||||
branch: 'takt/20260128T0504-fix-auth',
|
branch: 'takt/20260128T0504-fix-auth',
|
||||||
@ -112,6 +120,7 @@ describe('confirmAndCreateWorktree', () => {
|
|||||||
// Then
|
// Then
|
||||||
expect(result.execCwd).toBe('/project/.takt/worktrees/20260128T0504-fix-auth');
|
expect(result.execCwd).toBe('/project/.takt/worktrees/20260128T0504-fix-auth');
|
||||||
expect(result.isWorktree).toBe(true);
|
expect(result.isWorktree).toBe(true);
|
||||||
|
expect(mockSummarizeTaskName).toHaveBeenCalledWith('fix-auth', { cwd: '/project' });
|
||||||
expect(mockCreateWorktree).toHaveBeenCalledWith('/project', {
|
expect(mockCreateWorktree).toHaveBeenCalledWith('/project', {
|
||||||
worktree: true,
|
worktree: true,
|
||||||
taskSlug: 'fix-auth',
|
taskSlug: 'fix-auth',
|
||||||
@ -121,6 +130,7 @@ describe('confirmAndCreateWorktree', () => {
|
|||||||
it('should display worktree info when created', async () => {
|
it('should display worktree info when created', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
mockSummarizeTaskName.mockResolvedValue('my-task');
|
||||||
mockCreateWorktree.mockReturnValue({
|
mockCreateWorktree.mockReturnValue({
|
||||||
path: '/project/.takt/worktrees/20260128T0504-my-task',
|
path: '/project/.takt/worktrees/20260128T0504-my-task',
|
||||||
branch: 'takt/20260128T0504-my-task',
|
branch: 'takt/20260128T0504-my-task',
|
||||||
@ -146,21 +156,39 @@ describe('confirmAndCreateWorktree', () => {
|
|||||||
expect(mockConfirm).toHaveBeenCalledWith('Create worktree?', false);
|
expect(mockConfirm).toHaveBeenCalledWith('Create worktree?', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass task as taskSlug to createWorktree', async () => {
|
it('should summarize Japanese task name to English slug', async () => {
|
||||||
// Given: Japanese task name
|
// Given: Japanese task name, AI summarizes to English
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
mockSummarizeTaskName.mockResolvedValue('add-auth');
|
||||||
mockCreateWorktree.mockReturnValue({
|
mockCreateWorktree.mockReturnValue({
|
||||||
path: '/project/.takt/worktrees/20260128T0504-task',
|
path: '/project/.takt/worktrees/20260128T0504-add-auth',
|
||||||
branch: 'takt/20260128T0504-task',
|
branch: 'takt/20260128T0504-add-auth',
|
||||||
});
|
});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await confirmAndCreateWorktree('/project', '認証機能を追加する');
|
await confirmAndCreateWorktree('/project', '認証機能を追加する');
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
|
expect(mockSummarizeTaskName).toHaveBeenCalledWith('認証機能を追加する', { cwd: '/project' });
|
||||||
expect(mockCreateWorktree).toHaveBeenCalledWith('/project', {
|
expect(mockCreateWorktree).toHaveBeenCalledWith('/project', {
|
||||||
worktree: true,
|
worktree: true,
|
||||||
taskSlug: '認証機能を追加する',
|
taskSlug: 'add-auth',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show generating message when creating worktree', async () => {
|
||||||
|
// Given
|
||||||
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
mockSummarizeTaskName.mockResolvedValue('test-task');
|
||||||
|
mockCreateWorktree.mockReturnValue({
|
||||||
|
path: '/project/.takt/worktrees/20260128T0504-test-task',
|
||||||
|
branch: 'takt/20260128T0504-test-task',
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await confirmAndCreateWorktree('/project', 'テストタスク');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockInfo).toHaveBeenCalledWith('Generating branch name...');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
173
src/__tests__/summarize.test.ts
Normal file
173
src/__tests__/summarize.test.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Tests for summarizeTaskName
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('../claude/client.js', () => ({
|
||||||
|
callClaude: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../utils/debug.js', () => ({
|
||||||
|
createLogger: () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { callClaude } from '../claude/client.js';
|
||||||
|
import { summarizeTaskName } from '../task/summarize.js';
|
||||||
|
|
||||||
|
const mockCallClaude = vi.mocked(callClaude);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('summarizeTaskName', () => {
|
||||||
|
it('should return AI-generated slug for Japanese task name', async () => {
|
||||||
|
// Given: AI returns a slug for Japanese input
|
||||||
|
mockCallClaude.mockResolvedValue({
|
||||||
|
agent: 'summarizer',
|
||||||
|
status: 'done',
|
||||||
|
content: 'add-auth',
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await summarizeTaskName('認証機能を追加する', { cwd: '/project' });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe('add-auth');
|
||||||
|
expect(mockCallClaude).toHaveBeenCalledWith(
|
||||||
|
'summarizer',
|
||||||
|
'Summarize this task: "認証機能を追加する"',
|
||||||
|
expect.objectContaining({
|
||||||
|
cwd: '/project',
|
||||||
|
model: 'haiku',
|
||||||
|
maxTurns: 1,
|
||||||
|
allowedTools: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return AI-generated slug for English task name', async () => {
|
||||||
|
// Given
|
||||||
|
mockCallClaude.mockResolvedValue({
|
||||||
|
agent: 'summarizer',
|
||||||
|
status: 'done',
|
||||||
|
content: 'fix-login-bug',
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await summarizeTaskName('Fix the login bug', { cwd: '/project' });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe('fix-login-bug');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clean up AI response with extra characters', async () => {
|
||||||
|
// Given: AI response has extra whitespace or formatting
|
||||||
|
mockCallClaude.mockResolvedValue({
|
||||||
|
agent: 'summarizer',
|
||||||
|
status: 'done',
|
||||||
|
content: ' Add-User-Auth! \n',
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await summarizeTaskName('ユーザー認証を追加', { cwd: '/project' });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe('add-user-auth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should truncate long slugs to 30 characters', async () => {
|
||||||
|
// Given: AI returns a long slug
|
||||||
|
mockCallClaude.mockResolvedValue({
|
||||||
|
agent: 'summarizer',
|
||||||
|
status: 'done',
|
||||||
|
content: 'this-is-a-very-long-slug-that-exceeds-thirty-characters',
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await summarizeTaskName('長いタスク名', { cwd: '/project' });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.length).toBeLessThanOrEqual(30);
|
||||||
|
expect(result).toBe('this-is-a-very-long-slug-that-');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "task" as fallback for empty AI response', async () => {
|
||||||
|
// Given: AI returns empty string
|
||||||
|
mockCallClaude.mockResolvedValue({
|
||||||
|
agent: 'summarizer',
|
||||||
|
status: 'done',
|
||||||
|
content: '',
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await summarizeTaskName('test', { cwd: '/project' });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe('task');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use custom model if specified', async () => {
|
||||||
|
// Given
|
||||||
|
mockCallClaude.mockResolvedValue({
|
||||||
|
agent: 'summarizer',
|
||||||
|
status: 'done',
|
||||||
|
content: 'custom-task',
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await summarizeTaskName('test', { cwd: '/project', model: 'sonnet' });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCallClaude).toHaveBeenCalledWith(
|
||||||
|
'summarizer',
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
model: 'sonnet',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove consecutive hyphens', async () => {
|
||||||
|
// Given: AI response has consecutive hyphens
|
||||||
|
mockCallClaude.mockResolvedValue({
|
||||||
|
agent: 'summarizer',
|
||||||
|
status: 'done',
|
||||||
|
content: 'fix---multiple---hyphens',
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await summarizeTaskName('test', { cwd: '/project' });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe('fix-multiple-hyphens');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove leading and trailing hyphens', async () => {
|
||||||
|
// Given: AI response has leading/trailing hyphens
|
||||||
|
mockCallClaude.mockResolvedValue({
|
||||||
|
agent: 'summarizer',
|
||||||
|
status: 'done',
|
||||||
|
content: '-leading-trailing-',
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await summarizeTaskName('test', { cwd: '/project' });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe('leading-trailing');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -38,6 +38,7 @@ import { listWorkflows } from './config/workflowLoader.js';
|
|||||||
import { selectOptionWithDefault, confirm } from './prompt/index.js';
|
import { selectOptionWithDefault, confirm } from './prompt/index.js';
|
||||||
import { createWorktree } from './task/worktree.js';
|
import { createWorktree } from './task/worktree.js';
|
||||||
import { autoCommitWorktree } from './task/autoCommit.js';
|
import { autoCommitWorktree } from './task/autoCommit.js';
|
||||||
|
import { summarizeTaskName } from './task/summarize.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
||||||
|
|
||||||
const log = createLogger('cli');
|
const log = createLogger('cli');
|
||||||
@ -50,6 +51,7 @@ export interface WorktreeConfirmationResult {
|
|||||||
/**
|
/**
|
||||||
* Ask user whether to create a worktree, and create one if confirmed.
|
* Ask user whether to create a worktree, and create one if confirmed.
|
||||||
* Returns the execution directory and whether a worktree was created.
|
* Returns the execution directory and whether a worktree was created.
|
||||||
|
* Task name is summarized to English by AI for use in branch/worktree names.
|
||||||
*/
|
*/
|
||||||
export async function confirmAndCreateWorktree(
|
export async function confirmAndCreateWorktree(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
@ -61,9 +63,13 @@ export async function confirmAndCreateWorktree(
|
|||||||
return { execCwd: cwd, isWorktree: false };
|
return { execCwd: cwd, isWorktree: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Summarize task name to English slug using AI
|
||||||
|
info('Generating branch name...');
|
||||||
|
const taskSlug = await summarizeTaskName(task, { cwd });
|
||||||
|
|
||||||
const result = createWorktree(cwd, {
|
const result = createWorktree(cwd, {
|
||||||
worktree: true,
|
worktree: true,
|
||||||
taskSlug: task,
|
taskSlug,
|
||||||
});
|
});
|
||||||
info(`Worktree created: ${result.path} (branch: ${result.branch})`);
|
info(`Worktree created: ${result.path} (branch: ${result.branch})`);
|
||||||
|
|
||||||
|
|||||||
67
src/task/summarize.ts
Normal file
67
src/task/summarize.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Task name summarization using AI
|
||||||
|
*
|
||||||
|
* Generates concise English summaries for use in branch names and worktree paths.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { callClaude } from '../claude/client.js';
|
||||||
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
|
||||||
|
const log = createLogger('summarize');
|
||||||
|
|
||||||
|
const SUMMARIZE_SYSTEM_PROMPT = `You are a helpful assistant that generates concise English slugs for git branch names.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Output ONLY the slug, nothing else
|
||||||
|
- Use lowercase letters, numbers, and hyphens only
|
||||||
|
- Maximum 30 characters
|
||||||
|
- No leading/trailing hyphens
|
||||||
|
- Be descriptive but concise
|
||||||
|
- If the input is already in English and short, simplify it
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- "認証機能を追加する" → "add-auth"
|
||||||
|
- "Fix the login bug" → "fix-login-bug"
|
||||||
|
- "worktreeを作るときブランチ名をAIで生成" → "ai-branch-naming"
|
||||||
|
- "Add user registration with email verification" → "add-user-registration"`;
|
||||||
|
|
||||||
|
export interface SummarizeOptions {
|
||||||
|
/** Working directory for Claude execution */
|
||||||
|
cwd: string;
|
||||||
|
/** Model to use (optional, defaults to haiku for speed) */
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summarize a task name into a concise English slug using AI.
|
||||||
|
*
|
||||||
|
* @param taskName - Original task name (can be in any language)
|
||||||
|
* @param options - Summarization options
|
||||||
|
* @returns English slug suitable for branch names
|
||||||
|
*/
|
||||||
|
export async function summarizeTaskName(
|
||||||
|
taskName: string,
|
||||||
|
options: SummarizeOptions
|
||||||
|
): Promise<string> {
|
||||||
|
log.info('Summarizing task name', { taskName });
|
||||||
|
|
||||||
|
const response = await callClaude('summarizer', `Summarize this task: "${taskName}"`, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
model: options.model ?? 'haiku',
|
||||||
|
maxTurns: 1,
|
||||||
|
systemPrompt: SUMMARIZE_SYSTEM_PROMPT,
|
||||||
|
allowedTools: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const slug = response.content
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '')
|
||||||
|
.slice(0, 30);
|
||||||
|
|
||||||
|
log.info('Task name summarized', { original: taskName, slug });
|
||||||
|
|
||||||
|
return slug || 'task';
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user